Skip to main content
Every error response from the v1 API follows RFC 7807 application/problem+json. Branch on the code field — it’s stable. The title and detail are for humans and may be reworded over time.

Shape

{
  "type": "https://api.neuraldraft.io/errors/validation_failed",
  "title": "Validation failed",
  "status": 422,
  "code": "validation_failed",
  "detail": "One or more fields failed validation.",
  "instance": "req_2Nh4PqRsTuVw",
  "errors": {
    "customer_email": ["The customer email field must be a valid email."],
    "starts_at": ["The starts at field must be a valid ISO 8601 date."]
  }
}
FieldNotes
typeURI describing the error class. Stable, dereferenceable.
titleHuman-readable summary. Don’t pattern-match on this.
statusHTTP status code (mirrors the response status).
codeStable machine identifier. Branch on this.
detailHuman-readable explanation of this particular error.
instanceRequest id. Also returned as the X-Request-Id response header.
errorsField-level errors (only on 422).
Always log the instance (request id) — when you open a support ticket, that’s the first thing we’ll ask for.

Code catalog

HTTPcodeMeaningRetry?
400bad_requestMalformed request — invalid JSON, missing required body field, wrong method.No. Fix the request.
401unauthorizedMissing, malformed, expired, or revoked API key.No. Re-issue the key.
402insufficient_creditsProject balance is too low to cover the operation. Top up or upgrade.No, until credits are added.
403forbiddenKey is valid but lacks the required scope.No. Re-issue with the missing scope.
404not_foundResource does not exist (or your key can’t see it).No.
409conflictGeneric conflict — e.g. slug already taken, key limit reached.Sometimes. Read detail.
409slot_unavailableBooking slot was taken between availability check and create.Yes — refresh availability.
409idempotency_conflictIdempotency key reused with different parameters.No. Use a fresh key or match params.
409connect_not_readyStripe Connect not onboarded; checkout cannot be created.No, until Connect is set up.
422validation_failedOne or more fields failed validation. See errors map.No. Fix and resend.
429rate_limitedPer-project rate limit exceeded. Honour Retry-After.Yes — exponential backoff.
500internal_errorServer bug. We’re paged.Yes — exponential backoff, alert.
502upstream_unavailableUpstream AI provider error (model, image gen, etc.). Often transient.Yes — exponential backoff.
503service_unavailableMaintenance or transient outage. Honour Retry-After.Yes — exponential backoff.

insufficient_credits

The most common failure for active projects. Returned whenever a write or generation costs more credits than the project has on hand. The body includes cost (credits the operation needs) and balance (credits the project had on hand) so clients can render an exact top-up prompt:
{
  "type": "https://neuraldraft.com/errors/payment-required",
  "title": "Payment Required",
  "status": 402,
  "code": "insufficient_credits",
  "detail": "This operation requires 1 credit but the project balance is 0. Top up or upgrade your plan.",
  "cost": 1,
  "balance": 0,
  "instance": "req_2Nh4PqRsTuVw"
}
Every mutating write returns this shape on a broke project — including the 1-credit operations (content_update, page_update, image_register) as well as the larger generations (image, blog, video, website). Watch the credits_remaining field on GET /v1/projects/me/usage (or react to a 402 in your client) to alert your team or auto-top-up before mutating writes start failing.

validation_failed

Field-level errors are returned as an errors map; arrays of strings keyed by the offending field. Mirror the keys back to the user.
{
  "code": "validation_failed",
  "status": 422,
  "errors": {
    "customer_email": ["The customer email field must be a valid email."],
    "starts_at": ["The starts at field must be a valid ISO 8601 date."]
  }
}

rate_limited

The response carries Retry-After (seconds) and the standard rate-limit headers. See rate-limits for the full backoff strategy.

slot_unavailable

A booking-specific 409: the slot was taken between the time you ran an availability check and the time you posted the booking. Refresh availability and let the user pick again. Don’t auto-retry.

Retry strategy

For retryable error classes, use exponential backoff with full jitter. Cap retries at 5 attempts; cap any single delay at 30 seconds.
async function withRetry<T>(
  fn: () => Promise<T>,
  options: { maxAttempts?: number; baseMs?: number; capMs?: number } = {}
): Promise<T> {
  const { maxAttempts = 5, baseMs = 250, capMs = 30_000 } = options;
  let attempt = 0;
  let lastError: unknown;
  while (attempt < maxAttempts) {
    try {
      return await fn();
    } catch (err: any) {
      lastError = err;
      const status = err?.status;
      const code = err?.body?.code;
      const retryable =
        status === 429 ||
        status === 502 ||
        status === 503 ||
        code === "upstream_unavailable";
      if (!retryable) throw err;

      const retryAfter = Number(err?.headers?.["retry-after"]);
      const expBackoff = Math.min(capMs, baseMs * 2 ** attempt);
      const jittered = Math.random() * expBackoff;
      const delay = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : jittered;
      await new Promise((r) => setTimeout(r, delay));
      attempt++;
    }
  }
  throw lastError;
}
A few rules of thumb:
  • Never retry 400, 401, 403, 404, 409 (except slot_unavailable, which means “refresh and let the user choose”), or 422. They are deterministic.
  • Always honour Retry-After if present; the platform sets it precisely.
  • Long-running jobs are submitted via 202 Accepted and tracked via /jobs/{id}. The submit call is idempotent if you pass an Idempotency-Key; the job itself is the right place to handle failures, not the submit endpoint.

Idempotency

POST and other mutating endpoints accept an Idempotency-Key header (any unique string up to 255 chars). Retries with the same key within 24 hours return the original response without re-executing the side effect.
curl -X POST https://api.neuraldraft.io/v1/blog-posts \
  -H "Authorization: Bearer $NEURALDRAFT_API_KEY" \
  -H "Idempotency-Key: post-$(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"title":"Hello","content":"<p>Hi</p>","language_code":"en"}'
If you reuse a key with different parameters (different body, different path), the request fails with 409 idempotency_conflict. Use one key per logical operation; UUIDs are fine.

When to ask for help

Open a ticket with info@neuraldraft.io or the dashboard support widget and include:
  1. The instance (request id) — also X-Request-Id on the response.
  2. The exact request URL and method.
  3. The response status and code.
  4. The approximate timestamp (UTC).
We’ll trace the request end-to-end and respond within one business day on the free tier, four hours on Build, and one hour on Scale.