API error codes: what each one means and how to fix it

Updated 2026-10-30

Every error uses one JSON shape with a stable code. Match on the code, not the message. The message can change. This page lists all 15 codes with the HTTP status, cause and fix.

The error shape

json
{
  "error": {
    "code": "invalid_answers",
    "message": "Some answers need a fix.",
    "details": {
      "fields": [{"id": "guests", "problem": "must be at most 5"}]
    }
  }
}
PropertyMeaning
codeA stable machine-readable string. Safe to branch on.
messageA sentence for people. It can change, so do not parse it.
detailsOptional. Extra data such as the list of fields that failed.

All codes

CodeHTTPCauseFix
unauthorized401The Authorization header is missing, malformed or holds an unknown key.Send Authorization: Bearer afk_.... Copy the key again or rotate it. See Authentication.
forbidden403Your key is valid but may not do this. On public pages it also means the human check (Turnstile) was missing or failed. details.reason says which.Use the key of the account that owns the form. On a public page, reload and complete the check.
not_found404The form, response, webhook or token does not exist.Check the id. Remember that deleting a form deletes its responses and webhooks.
validation_failed422The request body breaks a rule: a bad field id, a duplicate id, a choice field without options, more than 50 fields, an unknown property or a wrong type. A bad cursor also returns this code with status 400.Read details.errors, fix the request and send it again. See Forms and fields.
blocked_field422A field id, label or help text matches a blocked word such as password, PIN, card number, IBAN, passport or national ID. details.fields lists the field, where it matched and what matched.Remove the field. We do not collect secrets. See Acceptable use.
invalid_answers422The submitted answers do not fit the form. details.fields lists each field id and the problem.Fix the listed fields and send again. A fill token allows 5 attempts.
form_closed409The owner closed the form, or it reached close_at or max_responses. details.reason says closed, expired or limit_reached.Owners can reopen it with POST /v1/forms/{id}/reopen. Respondents should contact the owner.
quota_exceeded403A plan allowance is used up: 100 responses this month, 3 open forms, or 3 webhooks on one form. details holds the limit.Wait for the next calendar month (UTC), close a form you no longer need, remove a webhook, or move to a larger plan when one is on sale.
rate_limited429Too many requests in a short time. details holds limit, window_seconds and retry_after.Wait for Retry-After seconds and try again. See Rate limits and quotas.
token_expired410The fill token is older than 60 minutes, or it used up its 5 attempts.Press the card on the form page and copy a new prompt.
token_used410The fill token already stored a response.Copy a new prompt for another response.
payload_too_large413The request body is over 64 KB.Send less data. Shorten long text answers.
rate_limiter_unavailable503We could not check the rate limit, so we refuse the request instead of guessing.Wait a moment and try again.
service_unavailable503A service we depend on, such as email for magic links, is not available.Try again later. If it keeps happening, email hello@aiformapi.com.
internal_error500Something broke on our side.Try again. If it keeps happening, email hello@aiformapi.com.

Handle errors in code

JavaScript

js
const res = await fetch("https://aiformapi.com/v1/forms", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AIFORMAPI_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(formDefinition),
});

if (!res.ok) {
  const { error } = await res.json();
  switch (error.code) {
    case "unauthorized":
      throw new Error("Check AIFORMAPI_KEY.");
    case "blocked_field":
    case "validation_failed":
      console.error("Fix the form:", error.details);
      break;
    case "quota_exceeded":
      console.error("Plan limit reached:", error.message);
      break;
    case "rate_limited":
      // Wait for Retry-After, then try again. See the rate limits page.
      break;
    default:
      throw new Error(`${res.status} ${error.code}: ${error.message}`);
  }
}

Python

python
import os
import requests

r = requests.post(
    "https://aiformapi.com/v1/forms",
    headers={"Authorization": f"Bearer {os.environ['AIFORMAPI_KEY']}"},
    json=form_definition,
    timeout=30,
)

if not r.ok:
    error = r.json()["error"]
    code = error["code"]
    if code == "unauthorized":
        raise SystemExit("Check AIFORMAPI_KEY.")
    if code in ("blocked_field", "validation_failed"):
        print("Fix the form:", error.get("details"))
    elif code == "quota_exceeded":
        print("Plan limit reached:", error["message"])
    elif code == "rate_limited":
        print("Wait", r.headers.get("Retry-After"), "seconds")
    else:
        raise RuntimeError(f"{r.status_code} {code}: {error['message']}")

Errors on the form page

People who fill a form by hand do not see JSON. The page shows plain messages. The closest to an error is "This form is full for this month", which appears when the form owner reached the response quota. The owner can read how to raise it in Rate limits and quotas.

Still stuck?

Check the exact paths and field names in openapi.json. It describes every endpoint.