# Errors

Every error body has the same shape.

```json
{
  "error": {
    "code": "invalid_limit",
    "message": "limit must be between 1 and 200",
    "retryable": false
  }
}
```

- **`code`** is for your program. It is part of the contract and does not
  change.
- **`message`** is for a reader. It is written to be relayed to an owner.
- **`retryable`** says whether the same request can succeed later.

Read `retryable` rather than guess from the status, and rather than guess
from the code. One code can be retryable on one call and not on another,
because the two calls have two remedies.

## Three fields that come and go

- **`next_action`** is the remedy, in plain language, where this surface
  knows one. Relay it to the owner. It is written for that.
- **`request_id`** names the one request. Only `internal` carries it: a
  fault on our side tells you nothing you can act on, so it gives you the
  one thing an operator can look up.
- **`detail`** carries a failed build's log. That log is the compiler
  telling whoever wrote the source what is wrong with it. `detail` is for
  build logs and nothing else.

**An absent `next_action` is a message too.** It says the `message` names
everything there is, and that this surface knows no call that mends it.
Do not invent one.

A resource that is not finished the moment you ask for it carries
`status` and `next_action` of its own, on a 200. Same field, same job.

## Four codes you meet everywhere

| Code | Status | Retryable | When |
|---|---|---|---|
| `internal` | 500 | yes | A fault on our side. Send the request again. If it keeps failing, `POST /v1/feedback` with the `request_id`. |
| `provider_error` | 502 | it depends | An outside service failed. `retryable` says whether it had a temporary problem or refused. The message names which part of the platform, never the provider behind it. |
| `plan_limit` | 402 | no | A cap the account's plan sets, or the size of what you sent. Time does not clear it. |
| `rate_limited` | 429 | yes | A cap on how often. Wait, then send the same request again. Do not send a different one. |

**There are two cap codes and no more.** A cap that money or a smaller
request lifts is `plan_limit`. A cap that a wait lifts is `rate_limited`.
Every cap on this surface is one of the two.

## Every code

`retryable` is **no** unless this table says otherwise.

### The request

| Code | Status | Retryable | When |
|---|---|---|---|
| `unauthorized` | 401 | no | No token, or a token this platform does not know. |
| `access_denied` | 403 | no | The owner refused the access request. Do not open another one unless they ask. |
| `owner_only` | 403 | no | A collaborator asked for something only the site's owner may do. |
| `domain_not_the_owners` | 403 | no | The domain and the site belong to different accounts. |
| `not_found` | 404 | no | No such thing, or none you may see. |
| `invalid_json` | 400 | no | The body is not valid JSON for this endpoint. |
| `body_too_large` | 400 | no | The body is over 1 MiB. |
| `invalid_cursor` | 400 | no | Use `next_cursor` from an earlier answer. |
| `invalid_limit` | 400 | no | `limit` is outside the range this call takes. |
| `invalid_since` | 400 | no | `since` is not an RFC3339 timestamp. |
| `invalid_period` | 400 | no | `period` is not one this call reports on. |
| `invalid_query` | 400 | no | `query` is missing or empty. |
| `invalid_format` | 400 | no | `format` is not one this call writes. |
| `invalid_scope` | 400 | no | `scope` is not one this call publishes. |
| `invalid_status` | 400 | no | `status` is not one this call filters on. |
| `nothing_to_update` | 400 | no | The body asks for no change. |
| `not_a_person` | 400 | no | The challenge was not solved, or not on this page. |
| `invalid_origin` | 400 | no | The call came from a page this surface does not serve. |

### Sign-in and tokens

| Code | Status | Retryable | When |
|---|---|---|---|
| `auth_disabled` | 503 | no | Sign-in by email is off on this deployment. |
| `signin_unavailable` | 410 | no | The owner approved, and no account exists for the email. |
| `invalid_email` | 400 | no | The email is not a plain valid address. |
| `expired` | 410 | no | The access request ran out before the owner answered. |
| `already_redeemed` | 410 | no | That sign-in is finished. Start a new one. |
| `not_a_grant` | 409 | no | The subscription was paid for, not granted. |
| `current_token` | 409 | no | A token cannot revoke itself. Mint another one first. |
| `invalid_expiry` | 400 | no | `expires_at` is not in the future. |
| `invalid_name` | 400 | no | A name is missing or empty. |

### Sites and collaborators

| Code | Status | Retryable | When |
|---|---|---|---|
| `invalid_title` | 400 | no | The title is missing or too long. |
| `invalid_template` | 400 | no | No template of that name. |
| `invalid_return_path` | 400 | no | The return path is not one this site serves. |
| `site_archived` | 409 | no | The site is archived, so it serves nothing. |
| `no_repositories` | 503 | no | This deployment holds no repository root. |
| `invitations_unavailable` | 503 | no | This deployment cannot send email, so it cannot invite. |
| `invalid_invitation` | 400 | no | The invitation link is not one this platform wrote. |
| `invitation_accepted` | 409 | no | That invitation is already taken. |
| `email_taken` | 409 | no | Another account already uses that email. |

### Source, publishing and deployments

| Code | Status | Retryable | When |
|---|---|---|---|
| `publish_disabled` | 503 | no | This deployment has no build capacity. Upload built output instead. |
| `no_source` | 409 | no | The site has no source yet. Push to its git repository. |
| `no_live_version` | 409 | no | Posts need a live site to build against. Publish the site. |
| `no_live_source` | 409 | no | The live version was not built from the source we hold. |
| `build_failed` | 422 | no | The site did not build. `detail` holds the log. |
| `build_timeout` | 422 | no | The build was stopped for taking too long. `detail` holds the log. |
| `invalid_redirects` | 400 | no | The site's redirect rules are not usable. |
| `missing_asset` | 409 | no | An image the site uses is gone from storage. |
| `invalid_manifest` | 400 | no | The manifest is empty, too large, or names a path this platform does not serve. |
| `invalid_path` | 400 | no | A path in the request is not one this platform stores. |
| `invalid_file` | 400 | no | The uploaded bytes are not a file this platform stores. |
| `hash_mismatch` | 400 | no | The uploaded bytes do not hash to the hash in the URL. |
| `size_mismatch` | 400 | no | The uploaded bytes are not the length the manifest gave. |
| `upload_incomplete` | 409 | no | Files the manifest names have not arrived. |
| `not_accepting_uploads` | 409 | no | The deployment is past the stage that takes files. |
| `superseded` | 409 | no | A newer deployment is live. Create a new one. |
| `not_restorable` | 409 | no | The deployment's files are not in the archive. Deploy from source. |
| `unarchived_content` | 409 | it depends | Files are missing from the archive. On a publish the same call fixes it; on a restore it does not. |
| `storage_error` | 502 | yes | Storage had a temporary problem. |
| `invalid_slot` | 400 | no | No preview slot of that number. |

### Previews

| Code | Status | Retryable | When |
|---|---|---|---|
| `preview_disabled` | 503 | no | This deployment cannot serve previews. Publish, and restore if needed. |
| `preview_empty` | 409 | no | The slot holds no draft yet. |
| `preview_expired` | 410 | no | The preview is marked for removal. Create a new one. |
| `preview_holds_work` | 409 | no | The slot holds work that is not on the site. |
| `slot_empty` | 409 | no | The slot holds no draft to act on. |
| `slot_not_branched` | 409 | no | The slot has no work of its own, so nothing can be brought into it. |
| `draft_behind` | 409 | no | The site moved on since this draft was built. |
| `draft_conflict` | 409 | no | The site and the slot changed the same files. |

### Domains

| Code | Status | Retryable | When |
|---|---|---|---|
| `invalid_domain` | 400 | no | The name is not one this platform can hold. |
| `invalid_mode` | 400 | no | The mode is not one this platform serves. |
| `invalid_record` | 400 | no | The DNS record is not one this platform writes. |
| `record_rejected` | 400 | no | The DNS provider refused the record as written. |
| `records_not_managed` | 409 | no | This platform does not hold this domain's DNS. |
| `cname_unavailable` | 503 | no | The address a CNAME points at is not ready. |
| `domain_taken` | 409 | no | Another account holds that domain. |
| `domain_unavailable` | 409 | no | That name is not one this platform gives out. |
| `domain_not_active` | 409 | no | The domain is not serving yet. |
| `attach_blocked` | 409 | no | Something about the domain stops a site going on it. |
| `check_unavailable` | 503 | yes | The check could not run for a moment. |

### Comments

| Code | Status | Retryable | When |
|---|---|---|---|
| `invalid_body` | 400 | no | The comment is missing or too long. |
| `no_storage` | 400 | no | This deployment cannot store a screenshot. |
| `note_too_long` | 400 | no | The note is over 500 characters. |

### Forms

| Code | Status | Retryable | When |
|---|---|---|---|
| `forms_disabled` | 503 | no | Forms are off on this deployment. |
| `invalid_fields` | 400 | no | A field is missing, repeated, or of a kind this platform does not take. |
| `invalid_notify_to` | 400 | no | The notify address is not a plain valid address. |
| `notify_to_not_on_site` | 400 | no | The notify address is not one this site holds. |
| `form_fields_removed` | 409 | no | The change drops fields that already carry submissions. |
| `form_has_submissions` | 409 | no | The form holds submissions, so it cannot be changed that way. |

### Bookings

| Code | Status | Retryable | When |
|---|---|---|---|
| `bookings_unavailable` | 503 | no | Bookings are off on this deployment, or this site takes none. Stop, and hide the form. |
| `invalid_timetable` | 400 | no | The timetable names no calendar, or no service. |
| `invalid_calendar` | 400 | no | Name one of the service's `calendar_keys`. |
| `invalid_service` | 400 | no | Name a service this site offers. |
| `invalid_visitor` | 400 | no | A name, an address or an intake field is missing or wrong. |
| `invalid_slot` | 400 | no | That start is not offered any more. Read the slots again. |
| `slot_taken` | 409 | no | Somebody took that time. Read the slots again. |
| `not_cancellable` | 409 | no | The booking is not live: a hold ran out, or it is already cancelled. |
| `payments_unavailable` | 400 | no | The timetable asks for money and this site takes none. |
| `nothing_paid` | 409 | no | Nobody paid, so there is nothing to give back. |
| `confirm_required` | 409 | no | The call changes something the caller has not confirmed. |

### Payments and payouts

| Code | Status | Retryable | When |
|---|---|---|---|
| `payouts_disabled` | 501 | no | This deployment cannot take money for a site owner. |
| `payouts_unavailable` | 503 | no | This deployment holds no sealing key, so it keeps no credential. |
| `payout_rail_unavailable` | 501 | no | This deployment does not offer that way of taking money. |
| `invalid_payout_account` | 400 | no | The payout account in the request is not usable. |
| `no_payout_account` | 409 | no | The site takes no money yet. |
| `payout_account_pending` | 409 | no | The payment provider will not let that account take money yet. |
| `payout_account_no_currency` | 409 | no | The provider has not said what the account is priced in. |
| `payout_account_gone` | 409 | no | The account the money went into is not used here any more. |
| `payout_key_unreadable` | 502 | no | This platform can no longer read the key held for that account. |
| `invalid_payment` | 400 | no | The payment in the request is not one this site can open. |
| `invalid_payout_offer` | 400 | no | The offer in the request is not usable. |
| `offer_expired` | 409 | no | The offer ran out before anybody answered. |
| `offer_already_accepted` | 409 | no | Both parties agreed already. |
| `not_yours_to_answer` | 409 | no | This account opened the offer, so the other party answers it. |

### Billing

| Code | Status | Retryable | When |
|---|---|---|---|
| `billing_disabled` | 501 | no | Nothing is metered on this deployment and no payment is required. |
| `invalid_plan` | 400 | no | No plan of that name. |
| `plan_unavailable` | 501 | no | That plan is not purchasable on this deployment. |
| `already_subscribed` | 409 | no | The account is on that plan already. |
| `no_billing_history` | 409 | no | The account never completed a payment, so there is no portal to open. |
| `paid_subscription_active` | 409 | no | A paid subscription is running. Cancel it before granting a plan. |

### Photos, uploads and sketches

| Code | Status | Retryable | When |
|---|---|---|---|
| `photos_disabled` | 503 | no | No stock photo library is configured. Add the photograph to the source yourself. |
| `photos_busy` | 503 | yes | The photo library is capping this whole platform, not you. Wait a minute. |
| `invalid_photo` | 400 | no | No photo of that id in the library. |
| `uploads_disabled` | 503 | no | This deployment cannot store files, or has no browser origin to hand over. |
| `invalid_upload` | 400 | no | The upload in the request is not usable. |
| `stale_hash` | 409 | no | The file changed while it was being stored. Send it again. |
| `sketches_disabled` | 503 | no | This deployment cannot host design sketches. Describe the directions instead. |
| `invalid_sketches` | 400 | no | A direction is missing something it needs. |

### Analytics and feedback

| Code | Status | Retryable | When |
|---|---|---|---|
| `analytics_disabled` | 503 | no | Analytics is off on this deployment. |
| `invalid_kind` | 400 | no | The feedback kind is not one this platform files. |
| `invalid_message` | 400 | no | The feedback message is missing. |
| `message_too_long` | 400 | no | The feedback message is over the limit. |
| `context_too_large` | 400 | no | The feedback context is over the limit. |

## Nothing provider-shaped leaks

No `/v1` answer names the hosting provider, its identifiers or its error
strings. A `provider_error` says which part of the platform failed, in
this platform's own words, and stops there.

Two answers carry a provider's address on purpose, because a payer's
browser has to land on it: a checkout URL and a portal URL.

## A message does not blame the wrong party

Clave works to keep each message about the party that can act on it. A
refusal names the consequence and the remedy for the surface you are on.

If a message sends you to fix something you did not break, that is a
defect here. Send it with `POST /v1/feedback`.

---

## Where this page sits

This is one page of the Clave documentation.
Read it as a web page at https://docs.heyclave.com/api/errors.
Every page has a `.md` address, and https://docs.heyclave.com/llms.txt lists them all.

- Previous: [Authentication](https://docs.heyclave.com/api/auth.md)
- Next: [Pagination](https://docs.heyclave.com/api/pagination.md)
- Index: [Overview](https://docs.heyclave.com/index.md)
