vat.toolsDocumentation
Open workspace

Errors, retries & idempotency

Error codes, safe retries, idempotency keys, and rate limits.

Errors are typed and carry a machine-readable code plus a retryability signal. Treating every failure as retryable — or as a negative business result — is the fast path to duplicate charges and wrong decisions.

Error shape

Failures return a JSON error body, and some carry retry metadata.

429 · RATE_LIMITED
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Retry after the interval in Retry-After."
  },
  "retryable": true,
  "retryAfterSeconds": 30
}
503 · STORAGE_UNAVAILABLE
{
  "error": {
    "code": "STORAGE_UNAVAILABLE",
    "message": "No successful save could be confirmed."
  },
  "retryable": true
}

Codes

FieldTypeDescription
UNAUTHORIZED401Missing, malformed, expired, or revoked key. Reissue a key.
SCOPE_FORBIDDEN403The key lacks the operation's scope.
ENVIRONMENT_FORBIDDEN403The key belongs to the other environment.
INVALID_INPUT400The input, freshness policy, or source capability is invalid for this request.
IDEMPOTENCY_CONFLICT409The key was reused with a different payload, or the operation is still unfinished. Read the code before retrying.
RESULT_EXPIRED410The saved result expired or was source-suppressed.
RATE_LIMITED429Admission backpressure. Honour Retry-After, add jitter, retry with the same key.
QUOTA_EXCEEDED429The monthly allowance is used. Wait for the next UTC monthly reset or review the confirmed entitlement. Top-up purchases are unavailable in this release.
INTERNAL_ERROR500Unexpected server error. No assumed result.
ACCESS_UNAVAILABLE503Access checks are temporarily down. Nothing was performed.
STORAGE_UNAVAILABLE503No successful save could be confirmed. Treat as an interrupted operation.

Idempotency

Every write operation accepts an Idempotency-Key header: 8–128 characters matching [A-Za-z0-9][A-Za-z0-9._:-]*. One key per logical check, not per HTTP attempt. A replay of the same key and payload returns the original operation and settles at zero units.

  1. Generate one key per logical check

    A random UUID is enough. Persist it with your own record before you send, so a retry after a crash can reuse it.

  2. Reuse it only for the same payload

    Reusing a key with a different payload is 409. A new check gets a new key.

  3. Read 409 and 410 before retrying

    These are not blind retries. Inspect the code: a conflict, or an expired or source-suppressed result, needs a decision.

Retry one logical operation
const idempotencyKey = crypto.randomUUID(); // persist this before sending

async function callWithRetry(body: unknown) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch("/api/v2/validations", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.VAT_TOOLS_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    if (response.ok) return response.json();
    if (response.status !== 429) return response.json(); // 400/401/403/409/410 need a decision

    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    const jitter = Math.random() * 400;
    await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 + jitter));
  }
  throw new Error("VAT Tools request did not settle");
}

Rate limits and quota

Accepted responses carry RateLimit-Limit, RateLimit-Remaining (when the counter is readable), and RateLimit-Reset in seconds. Two 429s mean different things:

  • RATE_LIMITED is admission backpressure. Wait for Retry-After, add jitter, and reuse the idempotency key.
  • QUOTA_EXCEEDED is the monthly allowance. Stop, surface the state, and let the next UTC monthly reset or a review of the confirmed entitlement resolve it. Top-up purchases are unavailable in this release. Never auto-overage.