Skip to content

Guide

Error envelope

Every 4xx and 5xx response uses the same JSON shape:

{
  "error": {
    "code": "BAD_REQUEST",
    "message": "Validation failed: email is required",
    "details": { "field": "email" },
    "correlationId": "00000000-0000-4000-8000-000000000005"
  }
}
FieldTypeDescription
error.codestringStable, safe-to-expose enum value. Switch on this in your client.
error.messagestringHuman-readable, sanitised of ARNs / JWTs / UUIDs. Safe to surface to end-users.
error.detailsobject?Optional structured detail when an HttpError carries one.
error.correlationIdstringRequest correlation ID, also echoed in the X-Correlation-Id response header. Include in every support request.

Status code catalog

CodeMeaningWhen it fires
400 BAD_REQUESTValidation or schema failureMissing required field, wrong type, malformed JSON.
401 UNAUTHORIZEDMissing or invalid authNo token / API key, expired JWT, bad signature.
402 PAYMENT_REQUIREDPlan limit reached or dunning grace expiredOrg over seat-cap, subscription past-due > 14 days.
403 FORBIDDENAuthenticated but not authorisedCross-org request, member touching admin-only field, owner self-demote.
404 NOT_FOUNDResource doesn’t exist or is soft-deletedBad UUID, deleted user / template.
409 CONFLICTOptimistic-concurrency mismatchversion field doesn’t match server state. See Idempotency.
412 PRECONDITION_FAILEDIf-Match / similar precondition failedRare; conditional-update flows.
413 PAYLOAD_TOO_LARGERequest body > 5 MB or render output > 30 KBImage upload too big, signature exceeded Outlook cap.
429 TOO_MANY_REQUESTSRate limit exceededSee Rate limits for limits and Retry-After.
500 INTERNAL_ERRORServer bug or unhandled exceptionTransient; retry with backoff. Open a support ticket if persistent.
503Service degradedHealth-check classifier; see /health.
StatusAction
400 / 403 / 404Don’t retry. Fix the request.
402Don’t retry. Check the /plan-status endpoint for the specific cap that hit.
409Read fresh, retry once. See Idempotency for the read-modify-write pattern.
413Don’t retry; shrink the payload.
429Honor Retry-After (seconds). Exponential backoff if no header (start 1s, max 30s, jitter).
5xxRetry with capped exponential backoff (start 250ms, max 8s, max 5 attempts).

Correlation IDs

Every request carries an X-Correlation-Id header (we generate one if you don’t send one). It rides through every Lambda log line and ends up in the audit table for write operations. Always include it in support requests — we can usually pinpoint a failure mode in under a minute given the ID.

curl -X POST "https://api.esigkit.com/v1/orgs/$ORG_ID/render/$USER_ID" \
  -H "Authorization: Bearer $ESIGKIT_TOKEN" \
  -H "X-Correlation-Id: my-app-trace-12345" \
  -i
# Response includes:
#   X-Correlation-Id: my-app-trace-12345

Retry pattern (Node + fetch)

async function callWithRetry(url: string, init: RequestInit, attempt = 0): Promise<Response> {
  const res = await fetch(url, init);
  if (res.ok) return res;

  // 429 — honor Retry-After
  if (res.status === 429 && attempt < 5) {
    const retryAfter = Number(res.headers.get('retry-after') ?? 2 ** attempt);
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    return callWithRetry(url, init, attempt + 1);
  }
  // 5xx — capped exponential backoff
  if (res.status >= 500 && res.status < 600 && attempt < 5) {
    await new Promise((r) => setTimeout(r, Math.min(8000, 250 * 2 ** attempt)));
    return callWithRetry(url, init, attempt + 1);
  }
  // 4xx (other than 429) — don't retry
  const body = await res.json().catch(() => ({}));
  throw new Error(`eSigKit ${res.status} ${body.error?.code}: ${body.error?.message}`);
}