Skip to main content
Search...C
Insertion API

Errors & Idempotency

Every endpoint returns a stable JSON envelope on failure. Status codes follow the standard HTTP conventions; error strings are short, machine-readable identifiers rather than free-form messages so callers can branch on them safely.

Error Envelope

Error response shape
{
  "ok": false,
  "error": "<machine_readable_code>"
}

The error field is a stable identifier (e.g. invalid_signature, rate_limited, scope_required:leads:write). Validation errors surface a comma-separated summary of the failed Zod constraints.

Status Codes

NameTypeDescription
200 OKsuccessRequest succeeded. POST returns 201 for inserted leads, 200 for patched/deduped.
201 CreatedsuccessNew lead inserted via POST.
400 Bad RequesterrorMalformed JSON, missing/invalid Zod field, or empty PATCH body. The error string describes the failure.
401 UnauthorizederrorMissing or invalid auth headers. Codes: missing_auth, invalid_public_key, invalid_signature, signature_expired, invalid_bearer.
403 ForbiddenerrorAuthenticated but lacking the required scope. Format: scope_required:<scope>.
404 Not FounderrorEndpoint disabled by feature flag, OR resource does not exist within your account. Returned generically to prevent cross-account probing.
405 Method Not AllowederrorUnsupported HTTP method for this route.
409 ConflicterrorA concurrent write changed the lead between read and write. Safe to retry.
429 Too Many RequestserrorRate limit exceeded. Honor the Retry-After header (seconds).
500 Internal Server ErrorerrorUnexpected server failure. Safe to retry with exponential backoff.

Idempotency

POST /api/v1/insertion/leads is idempotent on (account_id, campaign_id, external_lead_id). Re-submitting the same tuple with the same payload is a no-op (response status deduped). Re-submitting with mutated fields will patch the existing lead in place (response status patched).

POST response statuses
{
  "ok": true,
  "status": "inserted",  // first time the tuple was seen
  "lead": { "id": "uuid", "user_id": "uuid", "campaign_id": 123 }
}

{
  "ok": true,
  "status": "deduped",   // exact replay; nothing changed
  "lead": { "id": "uuid", "user_id": "uuid", "campaign_id": 123 }
}

{
  "ok": true,
  "status": "patched",   // same tuple, mutated fields applied
  "lead": { "id": "uuid", "user_id": "uuid", "campaign_id": 123 }
}

The optional Idempotency-Key request header is accepted and logged for your debugging, but the (account_id, campaign_id, external_lead_id) tuple is the canonical idempotency key.

Rate Limits

Rate limits are enforced per API key, sharing the bucket with the internal /api/leads/ingest path. When exceeded, the server returns 429 rate_limitedwith a Retry-After header (integer seconds). Sustained traffic above your ceiling will continue to 429 until the window rolls over.

Retry Strategy

Recommended retry policy:

  • 4xx (except 429): Do not retry. The error is a permanent client-side problem (bad payload, missing scope, wrong key). Fix and re-submit.
  • 429: Wait Retry-After seconds, then retry. If you see sustained 429s, contact support to raise your ceiling.
  • 5xx and network errors: Retry with exponential backoff (1s, 2s, 4s, 8s, capped at 30s) up to 5 attempts. POST is safely idempotent — retrying the same payload produces deduped on second attempt.
  • 409 conflict on PATCH: Re-read the lead via GET, merge your changes, and re-PATCH.