Skip to main content

Error Responses

All API errors return a consistent JSON envelope so your code can handle them uniformly regardless of which endpoint produced the error.

Table of Contents


Error Shape

Every error response uses the same structure:

{
"error": {
"code": "error_code_here",
"message": "Human-readable description of what went wrong.",
"request_id": "01JAXBKM3N4P5Q6R7S8T9UVWXY"
}
}
FieldTypeDescription
codestringMachine-readable error code (see table below)
messagestringHuman-readable description, suitable for logging but not for end users
request_idstringUnique ULID for tracing this request

The request_id is generated for every /flare/v1/* request (see Authentication). Include it when contacting support.


Idempotent Replay Header

When a mutating request (POST, PUT, PATCH, DELETE) is submitted with an Idempotency-Key header that matches a previously completed request, the API replays the original response exactly and sets:

Idempotent-Replay: true

The response body and HTTP status code are identical to the first delivery. This header is the only signal that the response is a replay — not a new execution. If the original request failed with a non-2xx response, the error is not cached and the request will be executed again.

Example: 201 Created replay

For a POST /flare/v1/jobs call replayed with the same Idempotency-Key, the second response repeats the original 201 Created (not 200 OK) and adds the replay header:

HTTP/1.1 201 Created
Content-Type: application/json
Idempotent-Replay: true

{
"id": "job_01J...",
"status": "pending",
"created_at": "2026-04-15T12:34:56Z"
}

The status code (201, 204, etc.) and body are byte-identical to the original delivery — only Idempotent-Replay: true tells you this is a replay.


Error Code Reference

:::tip Error code stability Error code strings are part of the public API contract. We will not remove or rename an existing code without a deprecation cycle — see the API stability and versioning policy for the full deprecation contract (12-month sunset on the API major, RFC 9745 / RFC 8594 headers, 410 Gone after sunset). Your application code is safe to branch on error.code values and on the typed exceptions in the SDKs.

HTTP status codes are also stable. Error message strings are NOT stable — they're human-readable and may be reworded at any time. Never branch on message content. :::

Authentication & Authorization (400 / 401 / 403 / 418)

HTTP StatusCodeWhen thrownFix
400missing_signatureStripe webhook request has no Stripe-Signature headerEnsure your webhook client sends the Stripe-Signature header
400invalid_signatureStripe/SendGrid webhook signature verification failed, or timestamp is outside the 5-minute replay windowVerify your signing secret matches the dashboard value; check clock skew
400invalid_idempotency_keyIdempotency-Key header is not 1–200 characters of [A-Za-z0-9_-]Use a UUID or random alphanumeric string as your idempotency key
401unauthorizedAPI key missing, malformed, or invalid; or JWT token missingInclude a valid Authorization: Bearer <key> header
401invalid_credentialsLogin attempt with wrong email or passwordCheck credentials and retry; use forgot-password if needed
403forbiddenAuthenticated user is not the project owner/adminUse a credential that belongs to the project owner
403email_not_verifiedLogin attempted before the account email was verifiedCheck your inbox and click the verification link
403account_suspendedAccount marked SuspendedAt by ops (returned by login before BCrypt for timing safety)Contact support to lift the suspension
403cannot_modify_ownerAttempt to PATCH or remove the project owner via the team-management endpointsTransfer ownership to another member first, then modify
403cannot_remove_ownerAttempt to DELETE the project owner from the membership listTransfer ownership first, then delete the user from the team
403role_exceeds_callerTried to assign a role higher than the caller's own (e.g. an admin attempting to make someone owner)Have the project owner perform the role change, or pick a role at or below the caller's rank
418security_notices_requiredSign-up was attempted without the required terms-and-security checkbox checkedRe-submit signup with terms_accepted_at set to the current timestamp

Not Found (404)

HTTP StatusCodeWhen thrownFix
404job_not_foundJob ID does not exist or belongs to a different projectVerify the job ID and that your API key belongs to the same project
404recurring_job_not_foundRecurring job ID does not exist for this projectVerify the recurring job ID
404project_not_foundProject ID does not exist or belongs to a different accountVerify the project ID
404account_not_foundAuthenticated user has no associated accountContact support if you believe this is incorrect
404alert_not_foundAlert ID does not exist for this projectVerify the alert ID
404member_not_foundProject member ID does not exist for this projectVerify the member ID and project context
404user_not_foundUser ID does not resolve to an account (typically on GET /platform/v1/auth/me after a deletion race)Re-authenticate; if persistent, contact support

Conflict (409)

HTTP StatusCodeWhen thrownFix
409idempotency_key_reuseAn idempotency key (Idempotency-Key header or the legacy body-field idempotency_key on POST /jobs) was reused with a different request body. Same-body replays return the original status code instead of 409 — this code only fires on body mismatch. (The older idempotency_conflict code was unified into this one and is no longer emitted; see Idempotency guide.)Use a new unique key for each logically distinct request
409last_projectAttempted to delete a project that's the user's only oneCreate another project first, or use DELETE /platform/v1/account to delete the entire account
409invalid_stateCancel or retry called on a job whose current state does not allow itCheck the job's status before attempting a state transition
409parent_terminalContinuation job submitted but the parent is cancelled or dead_letterDo not enqueue continuations after a parent reaches a terminal state
409recurring_job_type_conflictA recurring job with the same job_type already exists under a different IDFetch the existing recurring job by job_type before upserting
409key_revokedAPI key rotate called on a project whose key is permanently revokedCreate a new project; revoked keys cannot be reinstated
409already_revokedRevoke called on a project whose API key is already revokedNo action needed — the key is already permanently revoked
409email_already_registeredSign-up attempted with an email address that already has an accountUse the login endpoint or forgot-password flow

Quota & Rate Limiting (402 / 429)

HTTP StatusCodeWhen thrownFix
402billing_state_blockedProject is in past_due, unpaid, or cancelled billing stateResolve the outstanding invoice or reactivate the subscription via the dashboard billing portal
quota_exceededNot currently emitted. Export endpoints (POST /flare/v1/jobs/export, GET /platform/v1/projects/{id}/audit-log/export) return 429 rate_limit_exceeded instead when their 1-per-minute-per-tenant cooldown is in effect; honour the accompanying Retry-After headern/a — branch on rate_limit_exceeded from the Quota & Rate Limiting row below
429monthly_allowance_exceededMonthly job allowance is exhausted and opt-in overage is disabledWait for the billing period to reset, enable overage, or upgrade your plan. Check X-Quota-Reset for the period-end epoch
429spend_cap_reachedMonthly allowance is exhausted, overage is enabled, but the customer-set spend cap has been reachedRaise the spend cap, wait for the billing period to reset, or upgrade your plan
429rate_limit_exceededPer-project hourly request rate limit exceeded; OR per-IP auth-endpoint limit (20 failed login attempts per IP within a 15-minute sliding window, checked before password verification) exceeded for /platform/v1/auth/* POSTs; OR per-tenant export cooldown (1-per-minute on POST /flare/v1/jobs/export and GET /platform/v1/projects/{id}/audit-log/export)Back off until the Retry-After header value (seconds) elapses
429account_lockedPer-account failed-login limit (10 attempts / 15 min) was hit; checked before BCrypt for timing safetyWait for Retry-After seconds before retrying; reset password if you've forgotten it

Quota response headers (present on every POST /flare/v1/jobs response):

HeaderDescription
X-Quota-LimitMonthly job allowance for this project
X-Quota-UsedJobs created this billing period (pre-check; does not count replays)
X-Quota-OverageJobs created beyond the allowance this period
X-Quota-Overage-CostAccrued overage charge this period, in cents
X-Quota-Spend-CapOverage spend cap in cents (omitted when no cap is set)
X-Quota-ResetUnix epoch (seconds) of the billing-period end

Validation (400 / 422)

HTTP StatusCodeWhen thrownFix
400invalid_requestRequest body failed validation — missing required fields or out-of-range valuesSee Validation Error Details below
400invalid_cron_expressionCron expression could not be parsedUse a standard 5- or 6-part cron expression
400invalid_timezoneTimezone string is not a valid IANA identifierUse a tz database name such as America/New_York
400invalid_localeLocale string is not a recognised BCP-47 tagUse a tag like en-US, de-DE, fr-CA
400invalid_limitlimit query param was outside the endpoint's allowed range (e.g. invoices: 1–100). No silent clamping.Pass an explicit value within the documented range
400invalid_formatformat query param on an export endpoint was not jsonl or csvUse format=jsonl (default) or format=csv
400invalid_from/invalid_untilExport window param was missing or not a valid ISO-8601 timestampPass UTC timestamps, e.g. 2026-04-01T00:00:00Z
400invalid_rangeExport from was not strictly before untilReorder the params
400range_too_largeExport window exceeded 90 daysSplit into multiple smaller exports
400invalid_verification_tokenEmail verification attempted with a bad or expired tokenRequest a new verification email
400invalid_reset_tokenPassword reset attempted with a bad, expired, or already-used tokenStart the forgot-password flow again to get a fresh token
400confirm_requiredDELETE /platform/v1/account was missing the ?confirm=<email> param, or it didn't match the authenticated emailPass ?confirm=<your-account-email> exactly
422parent_not_foundparent_job_id does not reference an existing jobVerify the parent job ID before submitting a continuation
422invalid_roleTeam-invite or role-update body specified a role outside owner/admin/member/viewerPick one of the four supported roles
422invalid_emailEmail address was malformed (signup, invite)Use an RFC-5322-valid email
422invalid_tokenA generic single-use token (invite acceptance, etc.) was bad, expired, or already consumedRequest a fresh token from whoever issued it

Server & Integration (500 / 503)

HTTP StatusCodeWhen thrownFix
500internal_errorUnhandled server errorSafe to retry with exponential backoff; include request_id when contacting support
503billing_disabledBilling endpoint called but Stripe is not configured for this environmentContact your account manager or use the production environment
503stripe_unavailableStripe API call failed with StripeException (Stripe-side outage). The response distinguishes a Stripe-side outage from a Zeridion-side server error so you can retry against Stripe's status page.Retry after a short backoff; check status.stripe.com if the error persists

Validation Error Details

When validation fails, the API returns 400 with code: "invalid_request" and the message contains the first validation failure.

Create Job Validation

FieldRuleError message
job_typeRequired, non-empty"job_type is required."
job_typeMax 500 characters"job_type must not exceed 500 characters."
payloadRequired, non-null JSON"payload is required."
max_attempts1–100 (when provided)"max_attempts must be between 1 and 100."
timeout_seconds1–86,400 (when provided)"timeout_seconds must be between 1 and 86400."
queueMax 100 characters (when provided)"queue must not exceed 100 characters."
idempotency_keyMax 200 characters (when provided)"idempotency_key must not exceed 200 characters."
parent_job_idMax 36 characters (when provided)"parent_job_id must not exceed 36 characters."

Example validation error

{
"error": {
"code": "invalid_request",
"message": "job_type is required.",
"request_id": "01JAXBKM3N4P5Q6R7S8T9UVWXY"
}
}

Handling Errors in Code

The error envelope is identical across all three SDKs. Each maps the response to a typed exception class that carries code, message, request_id, and the HTTP status code, so you can branch on code for programmatic handling and surface message + request_id in your logs.

.NET / C#

try
{
await jobClient.EnqueueAsync<SendWelcomeEmail>(new { Email = "user@example.com" });
}
catch (FlareAuthenticationException)
{
// 401 — invalid or expired API key
}
catch (FlareRateLimitException ex)
{
// 429 — back off; ex.ResetAt has the window reset epoch
if (ex.ResetAt.HasValue)
{
var delay = ex.ResetAt.Value - DateTimeOffset.UtcNow;
if (delay > TimeSpan.Zero) await Task.Delay(delay);
}
}
catch (FlareConflictException ex) when (ex.ErrorCode == "idempotency_key_reuse")
{
// Same Idempotency-Key reused with a different request body — pick a new key
}
catch (FlareApiException ex)
{
Console.WriteLine($"Error {ex.StatusCode}: {ex.ErrorCode}{ex.Message}");
}

TypeScript / JavaScript

import {
FlareError,
AuthError,
RateLimitError,
ConflictError,
} from "@zeridion/flare";

try {
await flare.createJob({ job_type: "...", payload: { /* ... */ } });
} catch (err) {
if (err instanceof AuthError) {
// 401 — invalid API key
} else if (err instanceof RateLimitError) {
// 429 — back off; err.retryAfter has the window reset epoch
} else if (err instanceof ConflictError && err.code === "idempotency_key_reuse") {
// Same Idempotency-Key reused with a different request body
} else if (err instanceof FlareError) {
console.error(err.statusCode, err.code, err.requestId, err.message);
}
}

Python

from zeridion_flare import (
FlareError,
AuthError,
RateLimitError,
ConflictError,
)

try:
client.create_job({"job_type": "...", "payload": {}})
except AuthError:
... # 401
except RateLimitError as e:
... # 429 — e.retry_after has the window reset epoch
except ConflictError as e:
if e.code == "idempotency_key_reuse":
... # Same Idempotency-Key reused with a different request body
except FlareError as e:
print(e.status_code, e.code, e.request_id, e.message)

See the SDK Exceptions reference for the full .NET exception hierarchy.

See also