API error codes: what each one means and how to fix it
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
{
"error": {
"code": "invalid_answers",
"message": "Some answers need a fix.",
"details": {
"fields": [{"id": "guests", "problem": "must be at most 5"}]
}
}
}| Property | Meaning |
|---|---|
code | A stable machine-readable string. Safe to branch on. |
message | A sentence for people. It can change, so do not parse it. |
details | Optional. Extra data such as the list of fields that failed. |
All codes
| Code | HTTP | Cause | Fix |
|---|---|---|---|
unauthorized | 401 | The Authorization header is missing, malformed or holds an unknown key. | Send Authorization: Bearer afk_.... Copy the key again or rotate it. See Authentication. |
forbidden | 403 | Your 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_found | 404 | The form, response, webhook or token does not exist. | Check the id. Remember that deleting a form deletes its responses and webhooks. |
validation_failed | 422 | The 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_field | 422 | A 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_answers | 422 | The 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_closed | 409 | The 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_exceeded | 403 | A 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_limited | 429 | Too 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_expired | 410 | The 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_used | 410 | The fill token already stored a response. | Copy a new prompt for another response. |
payload_too_large | 413 | The request body is over 64 KB. | Send less data. Shorten long text answers. |
rate_limiter_unavailable | 503 | We could not check the rate limit, so we refuse the request instead of guessing. | Wait a moment and try again. |
service_unavailable | 503 | A 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_error | 500 | Something broke on our side. | Try again. If it keeps happening, email hello@aiformapi.com. |
Handle errors in code
JavaScript
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
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.