Error Handling
Error response format, error codes, and retry strategies for the ScamVerify™ API.
The ScamVerify™ API uses standard HTTP status codes and returns structured error responses. This guide covers the error format, all possible error codes, and recommended retry strategies.
Error Response Format
All errors follow a consistent JSON structure:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again in 12 seconds.",
"details": null,
"retry_after": 12,
"upgrade_url": null
}
}| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code (see table below) |
message | string | Human-readable error description |
details | object or null | Additional context for certain error types |
retry_after | number or null | Seconds to wait before retrying (only for rate limits) |
upgrade_url | string or null | Link to upgrade page (only for quota errors) |
Error Codes
| Code | HTTP Status | Description | Retryable |
|---|---|---|---|
missing_api_key | 401 | No API key provided in the Authorization header | No |
invalid_api_key | 401 | The API key is invalid, revoked, or expired | No |
validation_error | 400 | The request body is malformed or missing required fields | No |
rate_limit_exceeded | 429 | You have exceeded your plan's RPM limit | Yes |
quota_exhausted | 402 | Your monthly quota for this channel is used up | No |
internal_error | 500 | An unexpected server error occurred | Yes |
Validation Errors
When the request body fails validation, the details field contains an issues array describing each problem:
{
"error": {
"code": "validation_error",
"message": "Request validation failed",
"details": {
"issues": [
{
"field": "phone_number",
"message": "Phone number must be in E.164 format (e.g., +12025551234)"
},
{
"field": "force_refresh",
"message": "Expected boolean, received string"
}
]
},
"retry_after": null,
"upgrade_url": null
}
}Each issue in the array has:
| Field | Description |
|---|---|
field | The request body field that failed validation |
message | A description of what is wrong and how to fix it |
Retry Strategy
Not all errors should be retried. Here is how to handle each type:
| Status | Action |
|---|---|
| 400 (Validation) | Fix the request and resubmit. Do not retry automatically. |
| 401 (Auth) | Check your API key. Do not retry automatically. |
| 402 (Quota) | Upgrade your plan or wait for quota reset. Do not retry. |
| 429 (Rate Limit) | Wait for the retry_after duration, then retry. |
| 500 (Server Error) | Retry with exponential backoff. |
Never retry 400, 401, or 402 errors. These indicate a problem with your request or account, not a transient issue. Retrying them wastes resources and may trigger additional rate limiting.
Exponential Backoff with Jitter
For retryable errors (429 and 500), use exponential backoff with random jitter. This prevents multiple clients from retrying at the exact same time.
import time
import random
import requests
def scamverify_request(method, endpoint, api_key, payload=None, max_retries=5):
url = f"https://scamverify.ai/api/v1{endpoint}"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
for attempt in range(max_retries):
response = requests.request(method, url, headers=headers, json=payload)
# Success
if response.status_code < 400:
return response.json()
error_data = response.json().get("error", {})
status = response.status_code
# Non-retryable errors
if status in (400, 401, 402):
raise ApiError(
code=error_data.get("code"),
message=error_data.get("message"),
status=status,
)
# Rate limited: respect Retry-After
if status == 429:
retry_after = error_data.get("retry_after", 5)
jitter = random.uniform(0, retry_after * 0.25)
wait = retry_after + jitter
print(f"Rate limited. Waiting {wait:.1f}s (attempt {attempt + 1})")
time.sleep(wait)
continue
# Server error: exponential backoff
if status >= 500:
base_delay = 2 ** attempt # 1, 2, 4, 8, 16
jitter = random.uniform(0, base_delay * 0.5)
wait = base_delay + jitter
print(f"Server error. Retrying in {wait:.1f}s (attempt {attempt + 1})")
time.sleep(wait)
continue
raise ApiError(code="max_retries", message="Max retries exceeded", status=0)
class ApiError(Exception):
def __init__(self, code, message, status):
self.code = code
self.message = message
self.status = status
super().__init__(f"[{status}] {code}: {message}")
# Usage
try:
result = scamverify_request(
"POST",
"/phone/lookup",
api_key="sv_live_abc123...",
payload={"phone_number": "+12025551234"},
)
print(f"Verdict: {result['verdict']}")
except ApiError as e:
if e.code == "quota_exhausted":
print("Quota used up. Upgrade at:", e.message)
elif e.code == "invalid_api_key":
print("Check your API key")
else:
print(f"Error: {e}")class ApiError extends Error {
constructor(code, message, status) {
super(`[${status}] ${code}: ${message}`);
this.code = code;
this.status = status;
}
}
async function scamverifyRequest(method, endpoint, apiKey, payload, maxRetries = 5) {
const url = `https://scamverify.ai/api/v1${endpoint}`;
const headers = {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
};
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, {
method,
headers,
body: payload ? JSON.stringify(payload) : undefined,
});
// Success
if (response.ok) {
return await response.json();
}
const errorData = (await response.json()).error || {};
const status = response.status;
// Non-retryable errors
if ([400, 401, 402].includes(status)) {
throw new ApiError(errorData.code, errorData.message, status);
}
// Rate limited: respect Retry-After
if (status === 429) {
const retryAfter = errorData.retry_after || 5;
const jitter = Math.random() * retryAfter * 0.25;
const wait = retryAfter + jitter;
console.log(`Rate limited. Waiting ${wait.toFixed(1)}s (attempt ${attempt + 1})`);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
// Server error: exponential backoff
if (status >= 500) {
const baseDelay = 2 ** attempt; // 1, 2, 4, 8, 16
const jitter = Math.random() * baseDelay * 0.5;
const wait = baseDelay + jitter;
console.log(`Server error. Retrying in ${wait.toFixed(1)}s (attempt ${attempt + 1})`);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
}
throw new ApiError("max_retries", "Max retries exceeded", 0);
}
// Usage
try {
const result = await scamverifyRequest(
"POST",
"/phone/lookup",
"sv_live_abc123...",
{ phone_number: "+12025551234" }
);
console.log(`Verdict: ${result.verdict}`);
} catch (err) {
if (err.code === "quota_exhausted") {
console.log("Quota used up. Time to upgrade.");
} else if (err.code === "invalid_api_key") {
console.log("Check your API key");
} else {
console.error("Error:", err.message);
}
}Best Practices
-
Always parse the error response. Do not rely on HTTP status codes alone. The
codefield gives you the specific reason for the failure. -
Log errors with context. Include the
code,message, request endpoint, and timestamp in your logs for debugging. -
Handle quota exhaustion gracefully. Show a meaningful message to your users rather than a generic error. The
upgrade_urlfield links directly to the upgrade page. -
Set a retry budget. Limit the total number of retries (we recommend 5) and the total time spent retrying (we recommend 60 seconds) to avoid blocking your application indefinitely.
-
Use circuit breakers for 500 errors. If you receive multiple consecutive 500 errors, stop retrying and alert your operations team. Continued retries during an outage add unnecessary load.