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

# Errors

Every Shortcut API error type and code, what causes it, and how to fix it.

After this page you can handle every error the Shortcut API returns: you will know what caused it, whether to retry, and what to change. Every error body links to its section here.

## The error body

Every error has the same shape, on every endpoint:

```json
{
  "error": {
    "type": "permission",
    "code": "insufficient_scope",
    "message": "This token is missing the customers:write scope.",
    "requiredScope": "customers:write",
    "requestId": "b8f4c2e1-6d3a-4f7b-9e0c-1a2b3c4d5e6f",
    "docs": "https://docs.shortcutco.com/developers/concepts/errors#insufficient_scope"
  }
}
```

| Field           | Always present          | Meaning                                                                                         |
| --------------- | ----------------------- | ----------------------------------------------------------------------------------------------- |
| `type`          | Yes                     | The family of error; it decides the HTTP status. Branch on it for broad handling.               |
| `code`          | Yes                     | The specific error. Branch on it for precise handling. Codes are stable; new ones may be added. |
| `message`       | Yes                     | A sentence for a developer. It can change wording at any time; never parse it.                  |
| `param`         | When an input caused it | Where the problem is: `body.email`, `query.limit`, `query.cursor`, `headers.idempotency-key`.   |
| `requiredScope` | On `insufficient_scope` | The scope the token needs.                                                                      |
| `requestId`     | Yes                     | The same value as the `x-request-id` header. Quote it to support.                               |
| `docs`          | Yes                     | A link to this page at the code's section.                                                      |

Messages never repeat values you sent, so it is safe to log them.

## Types

| HTTP | `type`            | Codes                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400  | `invalid_request` | [`validation_failed`](#validation_failed), [`invalid_json`](#invalid_json), [`body_too_large`](#body_too_large), [`invalid_cursor`](#invalid_cursor), [`invalid_webhook_url`](#invalid_webhook_url)                                                                                                                                                                                                                                                                        |
| 401  | `unauthenticated` | [`token_missing`](#token_missing), [`token_invalid`](#token_invalid)                                                                                                                                                                                                                                                                                                                                                                                                       |
| 402  | `billing`         | [`subscription_required`](#subscription_required)                                                                                                                                                                                                                                                                                                                                                                                                                          |
| 403  | `permission`      | [`insufficient_scope`](#insufficient_scope), [`feature_unavailable`](#feature_unavailable)                                                                                                                                                                                                                                                                                                                                                                                 |
| 404  | `not_found`       | [`resource_not_found`](#resource_not_found)                                                                                                                                                                                                                                                                                                                                                                                                                                |
| 409  | `conflict`        | [`already_exists`](#already_exists), [`idempotency_key_in_use`](#idempotency_key_in_use), [`state_conflict`](#state_conflict), [`lead_is_customer`](#lead_is_customer), [`property_map_would_clear`](#property_map_would_clear), [`job_superseded`](#job_superseded), [`webhook_limit_reached`](#webhook_limit_reached), [`delivery_not_retryable`](#delivery_not_retryable), [`webhook_not_active`](#webhook_not_active), [`reminder_in_progress`](#reminder_in_progress) |
| 422  | `unprocessable`   | [`idempotency_key_reused_with_different_payload`](#idempotency_key_reused_with_different_payload), [`rule_violation`](#rule_violation), [`autopay_charge_required`](#autopay_charge_required), [`amount_mismatch`](#amount_mismatch), [`no_recipient`](#no_recipient), [`token_creator_unavailable`](#token_creator_unavailable)                                                                                                                                           |
| 429  | `rate_limited`    | [`too_many_requests`](#too_many_requests), [`workspace_rate_limited`](#workspace_rate_limited), [`invoice_recently_reminded`](#invoice_recently_reminded)                                                                                                                                                                                                                                                                                                                  |
| 503  | `unavailable`     | [`rate_limiter_unavailable`](#rate_limiter_unavailable), [`billing_unavailable`](#billing_unavailable), [`dependency_unavailable`](#dependency_unavailable), [`transaction_timeout`](#transaction_timeout), [`stop_completion_in_progress`](#stop_completion_in_progress), [`webhook_signing_unavailable`](#webhook_signing_unavailable)                                                                                                                                   |
| 500  | `internal`        | [`internal_error`](#internal_error)                                                                                                                                                                                                                                                                                                                                                                                                                                        |

Handle an unknown `code` by its `type`, so new codes never break your integration.

## invalid\_request

The request itself is wrong. Retrying it unchanged gets the same answer.

### validation\_failed

**Cause.** A path parameter, query parameter, header, or body field is missing, has the wrong type or format, is out of range, or is not accepted by this endpoint. `param` names the first problem and `message` says what was expected.

```json
{
  "error": {
    "type": "invalid_request",
    "code": "validation_failed",
    "message": "body.email: Must be a valid email.",
    "param": "body.email",
    "requestId": "0c9d8e7f-6a5b-4c3d-8e2f-1a0b9c8d7e6f",
    "docs": "https://docs.shortcutco.com/developers/concepts/errors#validation_failed"
  }
}
```

**Fix.** Correct the field named in `param` and send the request again. Common cases: `query.limit: Must be at most 200.`, a date that is not `YYYY-MM-DD`, an id that is not a UUID, a field name with a typo (bodies are strict, so unknown fields are refused rather than ignored). Send the corrected request with a new `Idempotency-Key`; see [Idempotency](/developers/concepts/idempotency-and-retries.md#idempotency-keys).

### invalid\_json

**Cause.** The body of a `POST`, `PUT`, or `PATCH` is not valid JSON. `param` is `body`.

**Fix.** Send a JSON object with `Content-Type: application/json`. In a shell, check your quoting: `-d '{"firstName": "Robin"}'` with single quotes outside and double quotes inside.

### body\_too\_large

**Cause.** The request body is larger than 1 MB.

**Fix.** Send less per request. No single record needs a body that size; split bulk work into one request per record.

### invalid\_cursor

**Cause.** The `cursor` is not one Shortcut issued, has expired, or was issued for different filters than this request has. `param` is `query.cursor`.

**Fix.** Repeat the request with exactly the filters the cursor came from, or drop `cursor` to start again from the first page. Treat cursors as opaque; never build or edit one.

### invalid\_webhook\_url

**Cause.** A webhook's `url`, in `POST /v1/webhooks` or `PATCH /v1/webhooks/{id}`, breaks a rule: it is not `https`, names a port other than 443, has a username or password in it, has a host name that does not resolve, or resolves to an address that is not public (private, loopback, link-local, carrier-grade NAT, multicast, documentation, reserved, or cloud-metadata). `param` is `body.url` and `message` says which rule. Nothing changed.

**Fix.** Use a public `https` URL on the standard port, with no credentials in it. To receive events on your own computer, use an HTTPS tunnel; see [Local development](/developers/webhooks/local-development.md).

## unauthenticated

Shortcut could not tell which token sent the request. Every `401` carries a `WWW-Authenticate` header.

### token\_missing

**Cause.** There is no `Authorization` header, or it has no bearer token.

```json
{
  "error": {
    "type": "unauthenticated",
    "code": "token_missing",
    "message": "Send an API token in the Authorization header: `Authorization: Bearer scut_...`.",
    "requestId": "3e2d1c0b-9a8f-4e7d-b6c5-4a3b2c1d0e9f",
    "docs": "https://docs.shortcutco.com/developers/concepts/errors#token_missing"
  }
}
```

**Fix.** Send `Authorization: Bearer scut_...` on every request. Tokens in the query string are never read.

### token\_invalid

**Cause.** The token is unknown, revoked, expired, or malformed. Shortcut answers all four the same way on purpose, so a guessed token learns nothing.

**Fix.** Check that the header is `Bearer` , one space, then the whole token, with nothing cut off when it was copied. Check **Settings → API & integrations** for whether the token was revoked or has expired; if so, create a new one. Many failed attempts from one address in a minute are answered [`too_many_requests`](#too_many_requests) for a minute.

## billing

### subscription\_required

**Cause.** The workspace's Shortcut subscription needs attention, so the API is paused for it, as the app is.

**Fix.** A company admin updates the subscription in **Settings → Plans & billing**. Requests work again as soon as it is in good standing. Do not retry in a loop.

## permission

### insufficient\_scope

**Cause.** The token is missing a scope this request needs. `requiredScope` names it. A request can also need a second scope because of what it would change: creating a lead whose email belongs to an existing customer needs `customers:write` as well as `leads:write`, and is refused before anything is created.

**Fix.** A company admin creates a new token with the scope in `requiredScope` and you replace the old token with it. Scopes cannot be added to an existing token. See [Scopes](/developers/concepts/scopes.md).

### feature\_unavailable

**Cause.** The record can be read through the API but not changed through it, such as a chemical service, whose settings live in the app's chemical settings. Nothing changed.

**Fix.** Make the change in the Shortcut app. A new token does not change this answer.

## not\_found

### resource\_not\_found

**Cause.** No record with that id exists in this token's workspace. Ids that belong to another workspace get the same answer, so a `404` never reveals whether an id exists elsewhere.

**Fix.** Check the id came from this workspace and from the right resource: a lead id is not a customer id. If you store ids from a sync, the record may have been replaced; list again.

## conflict

### already\_exists

**Cause.** The request would create a second record where only one is allowed: a service, product, or tag with the name of one the workspace already has, or an external id that already belongs to another record. Names are compared regardless of case, and deactivated services and archived products count. For a name clash, `param` is `body.name`. For an external id, `param` is `body.externalReference`, `body.services.<n>.externalReference` (jobs), or `body.externalId`, `message` names the record that has it, and nothing from the request was saved.

**Fix.** Do not retry. Use the existing record, or choose another name. To keep an external id in step with a record, use the external upsert (`PUT /v1/customers/external/{source}/{externalId}`, and the same path under properties, leads, and jobs); see [Sync](/developers/concepts/sync.md#external-references).

### email\_in\_use

**Cause.** Creating or changing a customer would give them an email that another customer, or a team member, in the workspace already has. `param` is `body.email`. When the email belongs to a customer, `message` names that customer's id; a team member is never named. Nothing changed.

**Fix.** Use the existing customer, or a different email. A customer's email is optional on create.

### external\_reference\_exists

**Cause.** `PUT /v1/customers/{id}/external-references/{source}` (or the property, lead, or job version) asked to link a record that already has a different id in that `source`. `param` is `params.source`. Nothing changed.

**Fix.** If the new id is right, unlink the old one with `DELETE` on the same path, then link again.

### external\_reference\_customer\_mismatch

**Cause.** `PUT /v1/properties/external/{source}/{externalId}` or `PUT /v1/jobs/external/{source}/{externalId}` named a `customerId`, but the id already belongs to a property or job of another customer. `param` is `body.customerId`. Nothing changed.

**Fix.** Send the customer the record belongs to (`GET` on the same path shows it), or use a different external id for the other customer's record.

### external\_reference\_changed

**Cause.** The external id was removed by another request while this upsert was running. Nothing changed.

**Fix.** Retry the same request.

### job\_superseded

**Cause.** A price change in the app replaced the job with a new version, and the request named the replaced version: the job external lookup, upsert, or list filter found an id still pointing at it, or a link named its id. The app moves a job's external ids to the new version when it replaces the job, so this means a link made from a stale job id. `message` names the current job. Nothing changed.

**Fix.** Use the job the message names. To keep your id on it, link it with `PUT /v1/jobs/{id}/external-references/{source}` after unlinking the old one.

### customer\_not\_inactive

**Cause.** `POST /v1/customers/{id}/reactivate` asked to resume jobs (`resumeJobs` `now` or `date`) for a customer who is not inactive. Nothing changed.

**Fix.** Nothing to reactivate. Resume a paused job with `POST /v1/jobs/{id}/resume`.

### idempotency\_key\_in\_use

**Cause.** Another request with the same `Idempotency-Key` (or MCP `requestKey`) is still running. `param` is `headers.idempotency-key`, and `Retry-After` is `1`.

**Fix.** Wait for `Retry-After` seconds and send the same request with the same key. You then get the first request's answer. See [Idempotency](/developers/concepts/idempotency-and-retries.md).

### state\_conflict

**Cause.** The record's current state does not allow this action, and the app would refuse it the same way: for example canceling a completed job, or deactivating a service that open estimates or jobs still use. `message` says which rule applies. Nothing changed.

**Fix.** Read the record again and act on its current `status`. To deactivate a service that is still in use, send `POST /v1/services/{id}/deactivate?force=true`; the jobs that use it keep it.

### lead\_is\_customer

**Cause.** You asked to dismiss a lead that stands for a customer: it is at the `customer` stage, or linked to an active customer account. Nothing changed.

**Fix.** Leave the lead as it is and manage the customer instead.

### webhook\_limit\_reached

**Cause.** The workspace already has 20 webhooks, the most it can have, whatever their status. Nothing was created.

**Fix.** Delete a webhook you no longer use with `DELETE /v1/webhooks/{id}`, or add event types to an existing webhook with `PATCH /v1/webhooks/{id}` instead of creating another.

### property\_map\_would\_clear

**Cause.** A `PATCH /v1/leads/{id}`, `PATCH /v1/properties/{id}`, or `PUT /v1/properties/external/{source}/{externalId}` moves the address to another spot, which clears the property map, its measured areas, and a square footage taken from that map. Shortcut refuses the change unless you confirm it. `message` says what would be removed, such as the number of measured areas and the measured square footage. `param` is `body.confirmClearPropertyMap`, and nothing changed. The external upsert treats an unchanged address as unchanged, whatever coordinates you send with it.

```bash
curl https://api.shortcutco.com/v1/leads/c2b9e7a1-4d3f-4b6e-9a8c-1f2e3d4c5b6a \
  -X PATCH \
  -H "Authorization: Bearer $SHORTCUT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "address": "900 Oak Ave, Bozeman, MT 59715", "addressLat": 45.6702, "addressLng": -111.0429 }'
```

```json
{
  "error": {
    "type": "conflict",
    "code": "property_map_would_clear",
    "message": "Moving this lead's address to another spot clears its property map and 2 measurements. Send confirmClearPropertyMap: true to go ahead; nothing was changed.",
    "param": "body.confirmClearPropertyMap",
    "requestId": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
    "docs": "https://docs.shortcutco.com/developers/concepts/errors#property_map_would_clear"
  }
}
```

**Fix.** If the move is intended, ask the person to confirm, then send the same body with `"confirmClearPropertyMap": true`. The response's `meta.effects` lists what was cleared: `property_map_cleared`, `measurements_cleared`, `sq_footage_cleared`.

### delivery\_not\_retryable

**Cause.** `POST /v1/webhooks/{id}/deliveries/{deliveryId}/retry` names a delivery that is not `failed` or `exhausted`: it was delivered, it is still waiting for an attempt, or an attempt of it is being sent right now. Test events cannot be retried either. Nothing changed.

**Fix.** Check the delivery's `status` in `GET /v1/webhooks/{id}/deliveries`. A `pending` or `held` delivery will be sent without your help. For a test event, send another with `POST /v1/webhooks/{id}/test`. See [Retries and monitoring](/developers/webhooks/retries-and-monitoring.md#retry-a-delivery).

### webhook\_not\_active

**Cause.** A delivery can be retried only while its webhook is `active`, and this one is `paused` or `disabled`. Nothing changed.

**Fix.** Set the webhook back to `active` with `PATCH /v1/webhooks/{id}` and `{ "status": "active" }`, then retry. Resuming a paused webhook already sends the events it held.

### reminder\_in\_progress

**Cause.** `POST /v1/invoices/{id}/remind` or `POST /v1/estimates/{id}/remind` repeated a request (the same `Idempotency-Key`, or no key within the same ten minutes) whose first attempt is still sending the reminder. Nothing was sent by this request.

**Fix.** Retry the same request, with the same `Idempotency-Key`, after `Retry-After` seconds. This refusal is not stored for the key. Once the first attempt finishes, the retry returns its reminder with `meta.alreadyReminded: true`. If the first attempt stopped before finishing, the retry sends the reminder itself, five minutes after the first attempt started; in that case the customer can receive it twice.

### estimate\_expired

**Cause.** `POST /v1/estimates/{id}/approve` reached an estimate past its expiry date. As in the app, the estimate is now marked `expired`, and that change is saved. No jobs were created.

**Fix.** Create a new estimate for the customer, or ask the office to reopen it in the app, and approve that.

### customer\_comms\_suppressed

**Cause.** `POST /v1/estimates/{id}/send` or `POST /v1/estimates/{id}/remind` reached a workspace whose customer messages are paused, so nothing was sent. `message` says why.

**Fix.** Resume customer messages in the workspace's settings in the app, then retry.

## unprocessable

### idempotency\_key\_reused\_with\_different\_payload

**Cause.** This `Idempotency-Key` was already used in the last 24 hours for a different request: another endpoint, or a different path, query, or body. `param` is `headers.idempotency-key`. Nothing ran.

**Fix.** Use a new key for each distinct write. This follows an attempt the write itself refused, such as one with an unusable phone number: that refusal was stored with the key, so the corrected request needs a fresh key.

### rule\_violation

**Cause.** The request breaks one of the app's rules for this record, such as setting a crew on a one-time job (only recurring jobs have a crew of their own; a one-time job's crew is set on its visit). `message` names the rule. Nothing changed.

**Fix.** Change the request to follow the rule in `message`. Retrying it unchanged gets the same answer.

### jobs\_not\_resumable

**Cause.** `POST /v1/customers/{id}/reactivate` with `resumeJobs` `now` or `date` reached a paused job that cannot be resumed from that date. `message` says why. Nothing changed: the customer is still inactive.

**Fix.** Reactivate with `resumeJobs: "keep_paused"`, then resume the jobs you can one at a time with `POST /v1/jobs/{id}/resume`.

### autopay\_charge\_required

**Cause.** `POST /v1/invoices/{id}/send` was called for a draft whose customer pays by autopay. Sending it in the app charges their saved payment method, and the API never starts a charge. Nothing was sent.

**Fix.** Send this invoice from the app. Drafts for customers without autopay can be sent through the API.

### amount\_mismatch

**Cause.** `POST /v1/invoices/{id}/payments` was called with an `amount` that is not the invoice's `total`. Recording an offline payment settles the invoice in full, as Mark paid does in the app; partial payments are not supported. `message` names the total. Nothing was recorded.

**Fix.** Read the invoice with `GET /v1/invoices/{id}` and send its `total` as `amount`.

### no\_recipient

**Cause.** `POST /v1/invoices/{id}/remind` asked for email, text, or both, but the customer has no email or phone on file for any channel asked for. Nothing was sent.

**Fix.** Add the customer's email or phone in the app, or ask only for a channel the customer has.

### token\_creator\_unavailable

**Cause.** The write is recorded on the customer's notes in the name of the person who created the token, and that person is no longer an admin of the workspace. Voiding an invoice and undoing a payment return this. Nothing changed.

**Fix.** Have a current admin create a new token in Settings and use it instead.

### service\_not\_supported

**Cause.** An estimate line's `serviceId` names a chemical or snow service. Those lines need terms the API does not take, such as application plans and storm triggers. `param` points at the line. Nothing was created.

**Fix.** Leave the line out and add it to the estimate in the app, or use a standard service.

### estimate\_not\_sendable

**Cause.** `POST /v1/estimates/{id}/send` refused the estimate as the app would: it has no lines, the customer has no email (or phone, for a text), or no channel was chosen. `message` says which. Nothing was sent.

**Fix.** Fix what `message` names, such as adding a line with `PATCH /v1/estimates/{id}` or sending by the channel the customer has, then send again.

### estimate\_not\_approvable

**Cause.** `POST /v1/estimates/{id}/approve` could not approve the estimate: it has no customer or property to create jobs for, or a line's chemical plan cannot be used. `message` says which. Nothing changed.

**Fix.** Fix the estimate in the app, then approve again.

### reminder\_not\_sent

**Cause.** `POST /v1/estimates/{id}/remind` found nowhere to send the reminder: the customer has no email or phone for the channels chosen, or every channel was skipped (unsubscribed, reported as spam, or opted out of texts). `message` gives the first reason. Nothing was sent, so the ten-minute window stays open.

**Fix.** Choose a channel the customer can receive, or update their contact details, then remind again.

## rate\_limited

Every `429` carries `Retry-After` in seconds. See [Rate limits](/developers/concepts/idempotency-and-retries.md#rate-limits).

### too\_many\_requests

**Cause.** This token went past its own limit (600 requests or 120 writes a minute), or this address sent too many requests with failing tokens.

```json
{
  "error": {
    "type": "rate_limited",
    "code": "too_many_requests",
    "message": "This token has reached its request limit. Retry after the time in the Retry-After header.",
    "requestId": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
    "docs": "https://docs.shortcutco.com/developers/concepts/errors#too_many_requests"
  }
}
```

**Fix.** Wait `Retry-After` seconds, then continue at a lower rate. Watch `RateLimit-Remaining` and slow down before it reaches zero.

### workspace\_rate\_limited

**Cause.** The workspace as a whole went past one of its shared windows: 2,000 requests a minute or 300 writes a minute across all its tokens, or 20 leads a minute created through the API. This code covers every workspace window.

**Fix.** Wait `Retry-After` seconds. Because the limit is shared, another integration in the same workspace may be using it; spread heavy jobs over time rather than adding tokens.

### invoice\_recently\_reminded

**Cause.** `POST /v1/invoices/{id}/remind` was called for an invoice already reminded through the API in the last ten minutes, by any token. Nothing was sent. A repeat of the request that sent that reminder, with the same `Idempotency-Key`, is not refused: it returns the first reminder with `meta.alreadyReminded: true`.

**Fix.** Wait `Retry-After` seconds before reminding this invoice again, or don't: the customer already has a recent reminder.

### estimate\_recently\_reminded

**Cause.** `POST /v1/estimates/{id}/remind` came within ten minutes of the estimate's last reminder. Nothing was sent. If the request carried an `Idempotency-Key`, this answer is stored for that key and replayed for it, even after the window.

**Fix.** Wait `Retry-After` seconds, then remind with a new `Idempotency-Key`.

## unavailable

A `503` is temporary. Every one carries `Retry-After`.

### follow\_up\_not\_queued

**Cause.** `POST /v1/estimates/{id}/approve` approved the estimate and created its jobs, but queueing their visits and the email to the workspace's admins failed. `Retry-After` is `5`.

**Fix.** Approve again after `Retry-After` seconds, with the same `Idempotency-Key` if you sent one. The retry re-queues the follow-up work and creates no jobs twice; its response has `meta.alreadyApproved: true`.

### rate\_limiter\_unavailable

**Cause.** Shortcut could not reach the service that counts requests, and this request is one whose limits must hold before it runs: writes that create leads, message customers, or change money.

**Fix.** Retry after `Retry-After` seconds with the same idempotency key. Reads are not affected.

### transaction\_timeout

**Cause.** The write's database work took too long, or could not start in time, and was rolled back. Nothing was saved. `Retry-After` is `5`.

**Fix.** Retry after `Retry-After` seconds. Reusing the same `Idempotency-Key` is safe: the retry runs the request again rather than replaying this error.

### stop\_completion\_in\_progress

**Cause.** Another `POST /v1/visits/{id}/complete` request with `completeStop=true` is completing the same stop right now. Nothing was done by this request. `Retry-After` is `5`.

**Fix.** Retry after `Retry-After` seconds. Reusing the same `Idempotency-Key` is safe: the retry runs the request again rather than replaying this error, and completes whatever the other request left open.

### billing\_unavailable

**Cause.** Shortcut could not confirm the workspace's subscription status for this request.

**Fix.** Retry after `Retry-After` seconds with the same idempotency key.

### webhook\_signing\_unavailable

**Cause.** Shortcut could not issue a webhook signing secret, so the webhook was not created or its secret was not rotated. Nothing changed. `Retry-After` is `3600`.

**Fix.** Retry after `Retry-After` seconds. If it keeps happening, contact Shortcut support with the `requestId`.

### dependency\_unavailable

**Cause.** A service the write depends on could not be reached, such as the payment provider when an online payment in progress must be canceled before an offline payment is recorded, or the notification queue when a reminder is sent. `message` says which. `Retry-After` is `30`.

**Fix.** Retry after `Retry-After` seconds with the same idempotency key.

## internal

### internal\_error

**Cause.** Something failed on Shortcut's side. It is recorded with the request id.

**Fix.** Retry with exponential backoff and the same idempotency key. If it keeps happening, contact Shortcut support with the `requestId`.

## Errors on the MCP server

The MCP server uses the same `type` and `code` vocabulary, so this page covers both.

* **A tool call that fails** returns a tool result with `isError: true`. Its `structuredContent` is exactly the error body above, and its text content is the same JSON. When the error says to wait, the result adds the sentence `Retry after N seconds.` and `_meta["com.shortcutco/retryAfterSeconds"]`.
* **A tool call that needs a scope the token lacks** is answered with HTTP `403` and a JSON-RPC error instead, with `error.data` holding the `insufficient_scope` body and a `WWW-Authenticate: Bearer error="insufficient_scope", scope="…"` header, so MCP clients can tell the user which scope to grant.
* **A missing or invalid token** is HTTP `401` with a JSON-RPC error whose `error.data` holds the `token_missing` or `token_invalid` body.
* **A rate-limited message** other than a tool call, such as `initialize` or `tools/list`, is HTTP `429` with JSON-RPC error code `-32002`, a `Retry-After` header, and the `too_many_requests` or `workspace_rate_limited` body in `error.data`. A rate-limited tool call is a tool result with `isError: true`, as above.

```json
{
  "jsonrpc": "2.0",
  "id": null,
  "error": {
    "code": -32001,
    "message": "The API token is invalid, expired, or revoked.",
    "data": {
      "error": {
        "type": "unauthenticated",
        "code": "token_invalid",
        "message": "The API token is invalid, expired, or revoked.",
        "requestId": "1f2e3d4c-5b6a-4978-8695-a4b3c2d1e0f9",
        "docs": "https://docs.shortcutco.com/developers/concepts/errors#token_invalid"
      }
    }
  }
}
```

See [MCP overview](/developers/mcp/overview.md) for the rest of the protocol details.
