Platform · Errors

Error handling

Savitar follows standard REST status semantics. 4xx codes mean fix your request; 5xx codes mean retry. Every error response is structured JSON.

HTTP status codes

CodeMeaningNotes
200OKRequest succeeded.
201CreatedResource created (e.g. new pattern).
204No ContentSuccessful, no body (e.g. delete).
400Bad RequestMalformed JSON or missing required field.
401UnauthorizedMissing or invalid API key.
403ForbiddenKey valid but lacks the required scope.
404Not FoundResource does not exist or was deleted.
409ConflictDuplicate (e.g. pattern name already exists).
422Validation ErrorSchema validation failed. Inspect `detail`.
429Rate LimitedBackoff and respect Retry-After.
5xxServer ErrorTransient. Retry with exponential backoff.

Validation errors

Bodies that fail schema validation return 422 with a detail array describing each violation.

422 Unprocessable Entity
{
  "detail": [
    {
      "loc": ["body", "text"],
      "msg": "field required",
      "type": "value_error.missing"
    },
    {
      "loc": ["body", "operator"],
      "msg": "value is not a valid enumeration member; permitted: 'replace', 'redact', 'mask', 'hash'",
      "type": "type_error.enum"
    }
  ]
}

Retry strategy

For 429 and 5xx responses, retry with exponential backoff (1s, 2s, 4s, 8s, 16s) and a maximum of five attempts. Honor the Retry-After header if present.

async function withRetry<T>(fn: () => Promise<Response>, max = 5): Promise<T> {
  let attempt = 0;
  while (true) {
    const res = await fn();
    if (res.ok) return res.json();
    if (attempt >= max - 1 || (res.status !== 429 && res.status < 500)) {
      throw new Error(`Request failed: ${res.status}`);
    }
    const retryAfter = Number(res.headers.get("Retry-After")) || 2 ** attempt;
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    attempt++;
  }
}