> For the complete documentation index, see [llms.txt](https://docs.powerfulform.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.powerfulform.com/public-api/errors.md).

# Errors

Every error code the Public API returns, what causes it, and what to do about it.

Errors always come back in the same shape, and the HTTP status is always meaningful:

```json
{
  "errors": [
    {
      "code": "insufficient_scope",
      "message": "This API key is missing the required scope: submissions:delete."
    }
  ]
}
```

Branch your code on `code`, not on `message`. Codes are a fixed contract; message wording can be improved at any time. `field` appears only when the error belongs to a specific input.

## Error codes

<table><thead><tr><th width="100">Status</th><th width="230">Code</th><th>Cause and fix</th></tr></thead><tbody><tr><td>401</td><td><code>unauthorized</code></td><td>No key reached the server. Check the header name — it is <code>X-PFB-API-KEY</code>, or <code>Authorization: Bearer &#x3C;key></code>. A key in the query string is never read.</td></tr><tr><td>401</td><td><code>invalid_api_key</code></td><td>The key does not exist, is mistyped, has been revoked, or has expired. Issue a new one in <strong>Settings → Public API</strong>.</td></tr><tr><td>403</td><td><code>insufficient_scope</code></td><td>The key is valid but lacks the scope named in the message. Scopes cannot be edited — issue a new key with the right permissions.</td></tr><tr><td>403</td><td><code>plan_upgrade_required</code></td><td>The store's plan does not include the Public API. See <a href="/settings/plans-and-billing.md">Plans and billing</a>.</td></tr><tr><td>403</td><td><code>cors_not_supported</code></td><td>The request carried an <code>Origin</code> header, so it came from a browser. Move the call to your server. If the key was ever in client-side code, revoke it — it is public now.</td></tr><tr><td>404</td><td><code>not_found</code></td><td>The record does not exist, or belongs to another store — the two are answered identically on purpose. Also returned for an unknown path under <code>/api/public/v1</code>.</td></tr><tr><td>409</td><td><code>form_inactive</code></td><td>You posted to a form whose status is Draft. Set it Active, or post to a different form.</td></tr><tr><td>415</td><td><code>unsupported_media_type</code></td><td>A body arrived without <code>Content-Type: application/json</code>.</td></tr><tr><td>422</td><td><code>validation_failed</code></td><td>A parameter or field value was rejected. <code>field</code> names the input; there may be several entries.</td></tr><tr><td>422</td><td><code>invalid_file_field</code></td><td>A file value could not be read, downloaded, or accepted. <code>field</code> names the field key. See <a href="/public-api/submissions/create-a-submission.md#uploading-files">Uploading files</a>.</td></tr><tr><td>422</td><td><code>too_many_keys</code></td><td>The store already holds the maximum number of active API keys. Revoke one first.</td></tr><tr><td>429</td><td><code>rate_limited</code></td><td>The per-minute budget for that bucket is spent. Wait for <code>Retry-After</code> seconds. See <a href="/public-api/requests-and-responses.md#rate-limits">Rate limits</a>.</td></tr></tbody></table>

## Validation errors

Validation reports everything wrong in one response. On [create](/public-api/submissions/create-a-submission.md) the `field` is prefixed with `data.` so it maps straight back to what you sent:

```json
{
  "errors": [
    { "code": "validation_failed", "field": "data.name",  "message": "The name field is required." },
    { "code": "validation_failed", "field": "data.email", "message": "The email must be a valid email address." }
  ]
}
```

Rules come from the form itself, so the messages match what a customer would see on the storefront. If a required field was added to the form after your integration was written, this is where it shows up.

## How to handle each class

<table><thead><tr><th width="120">Status</th><th>What your code should do</th></tr></thead><tbody><tr><td><strong>401, 403</strong></td><td>Stop and alert a human. Retrying cannot fix a wrong, revoked or under-scoped key.</td></tr><tr><td><strong>404</strong></td><td>Stop. The id is wrong or the record is gone; a retry returns the same answer.</td></tr><tr><td><strong>409, 415, 422</strong></td><td>Stop and fix the request. Do not retry the same payload — it will fail identically.</td></tr><tr><td><strong>429</strong></td><td>Wait <code>Retry-After</code> seconds, then retry. This is the one code worth an automatic retry.</td></tr><tr><td><strong>5xx</strong></td><td>Retry with backoff. On a write, send the same <a href="/public-api/submissions/create-a-submission.md#retrying-safely"><code>Idempotency-Key</code></a> so a retry cannot create a duplicate.</td></tr></tbody></table>

{% hint style="warning" %}
A timeout is not proof that nothing happened — the write may have succeeded before the connection dropped. Always send an `Idempotency-Key` when creating submissions, so the retry returns the original record instead of creating a second one.
{% endhint %}

<br>

## Next steps

* [Requests and responses](/public-api/requests-and-responses.md) — headers, envelope, pagination and rate limits.
* [Create an API key](/public-api/api-keys.md) — fix a scope problem by issuing the right key.
* [Contact support](/help/contact-support.md) — if a call fails in a way this page does not explain.
