HTTP status codes
| Code | Meaning | Action |
|---|
200 | Event accepted | No action needed |
400 | Bad request — missing field, invalid format, or inactive site_id | Fix the request payload |
401 | Missing X-API-Key header | Add the X-API-Key header |
403 | Invalid key, revoked key, or site_id/key mismatch | Check your API key and site_id |
413 | Payload too large | Reduce payload size or split into smaller batches |
422 | Validation error — see response body for details | Fix the specific field error |
429 | Rate limit exceeded | Back off and retry after the Retry-After header value |
500 / 502 / 503 | Server error | Retry with exponential backoff |
All error responses return a JSON body with details:
{
"detail": [
{
"loc": ["body", "scan_session_id"],
"msg": "field required",
"type": "value_error.missing"
}
]
}
For 400 errors with a single message:
{
"detail": "site_id not found or inactive"
}
Rate limits
| Endpoint | Limit | Scope |
|---|
POST /ct (browser events) | 100 requests / min | Per IP address |
POST /collect/batch | 100 requests / min | Per IP address |
POST /server-events | 1,000 requests / min | Per API key |
POST /server-events/batch | 1,000 requests / min | Per API key |
Short bursts above the limit are handled with a small grace window. Design your clients to back off on 429 rather than relying on burst tolerance.
Error handling pattern
For server events
async function sendEvent(payload) {
const response = await fetch('https://track.scanova.io/server-events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': process.env.SCANOVA_API_KEY,
},
body: JSON.stringify(payload),
});
if (response.ok) return await response.json();
const error = await response.json().catch(() => null);
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') ?? '60', 10);
throw new RetryableError(`Rate limited — retry after ${retryAfter}s`, retryAfter);
}
if (response.status >= 500) {
throw new RetryableError(`Server error ${response.status}`);
}
// 400, 401, 403, 422 — do not retry, fix the request
throw new PermanentError(`Request failed ${response.status}: ${JSON.stringify(error)}`);
}
Retry logic
Only retry on transient errors. Stop immediately on permanent errors.
| Retry? | Status codes |
|---|
| Yes — retry with backoff | 429, 500, 502, 503, 504, network timeout |
| No — fix the request | 400, 401, 403, 413, 422 |
See Idempotency & Retries for a full retry implementation with exponential backoff.
Common 422 causes
| Error | Fix |
|---|
scan_session_id invalid format | Must be a valid UUID (e.g. 7ad26d4f-3181-4ef8-b6ca-b8f59499dd43) |
event_name missing | Required for server events |
conversion_value.currency invalid | Must be a 3-letter ISO 4217 code (e.g. USD, EUR, GBP) |
properties too large | Keep under 10 KB |
Raw email in properties | Use user_identifiers.email_hash with a SHA-256 hash instead |