Skip to main content

HTTP status codes

CodeMeaningAction
200Event acceptedNo action needed
400Bad request — missing field, invalid format, or inactive site_idFix the request payload
401Missing X-API-Key headerAdd the X-API-Key header
403Invalid key, revoked key, or site_id/key mismatchCheck your API key and site_id
413Payload too largeReduce payload size or split into smaller batches
422Validation error — see response body for detailsFix the specific field error
429Rate limit exceededBack off and retry after the Retry-After header value
500 / 502 / 503Server errorRetry with exponential backoff

Error response format

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

EndpointLimitScope
POST /ct (browser events)100 requests / minPer IP address
POST /collect/batch100 requests / minPer IP address
POST /server-events1,000 requests / minPer API key
POST /server-events/batch1,000 requests / minPer 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 backoff429, 500, 502, 503, 504, network timeout
No — fix the request400, 401, 403, 413, 422
See Idempotency & Retries for a full retry implementation with exponential backoff.

Common 422 causes

ErrorFix
scan_session_id invalid formatMust be a valid UUID (e.g. 7ad26d4f-3181-4ef8-b6ca-b8f59499dd43)
event_name missingRequired for server events
conversion_value.currency invalidMust be a 3-letter ISO 4217 code (e.g. USD, EUR, GBP)
properties too largeKeep under 10 KB
Raw email in propertiesUse user_identifiers.email_hash with a SHA-256 hash instead