# Errors

> What the MISSIN API returns when a key is refused, an operation is not open to keys, a limit is reached or a mutation does not go through.

## A refused key

A key that is refused before anything runs gets an HTTP error status and one GraphQL error whose message is the code, also sent as `errors[0].extensions.code`. The message never says more than the code.

| Code | Status | Meaning |
|---|---|---|
| `api_key_invalid` | 401 | The key is malformed, unknown, revoked or expired. |
| `api_key_creator_not_member` | 401 | The member who created the key is no longer in the workspace. |
| `api_key_conflict` | 400 | The request sent two different keys, one as a bearer token and one in `x-api-key`. |
| `origin_not_allowed` | 403 | A publishable key was sent from an origin that is not on its list. This is checked before the key's kind. |
| `rate_limited` | 429 | The key, or the key text before it was checked, went over its limit. |
| `api_key_required` | 401 | The versioned public URL was called without an API key. |
| `unauthenticated` | 401 | A bearer token that is not a key was sent and is not a valid sign-in token. |

## An operation a key may not run

These are refused before anything in the operation runs, with an HTTP error status and the code as the message.

| Code | Status | Meaning |
|---|---|---|
| `operation_not_public` | 403 | The operation is not open to API keys. |
| `insufficient_access` | 403 | The key's access level is below what the operation needs. |
| `secret_key_required` | 403 | A publishable key was sent to the public API. Publishable keys are refused on `/graphql/v1` and `/mcp` for every operation, so use a secret key. |
| `connection_key_not_supported` | 403 | A key limited to one connection was sent to the API. Such a key is refused everywhere, because the API does not narrow it to its connection. |

## A version that does not exist

| Code | Status | Meaning |
|---|---|---|
| `unknown_api_version` | 400 | The `Missin-Version` header names no MISSIN API version. Leave it out for the latest, or send one from the [changelog](https://developer.missin.co.uk/changelog.md). A version that has been sunset is not refused: the request is served by the oldest supported version, and `Missin-Version` in the response says which. |

## Too many requests from one address

| Code | Status | Meaning |
|---|---|---|
| `RATE_LIMITED` | 429 | Over 200 requests a minute from one IP address. The code is in `errors[0].extensions.code`, and `Retry-After` says when to try again. |

## A mutation that did not go through

A mutation that fails for a reason you can fix returns `userErrors` in its payload, a list of `{ field, code, message }` entries, with the result `null`. An empty list means it worked. Branch on `code`, not on `message`.

## An order without a valid placed_at

Every order in MISSIN says when its buyer placed it, as `placed_at`. An order without one, or with one that does not name a moment in time, is refused whole: nothing is written, and MISSIN never fills the date in for you, not with the time it received the order or any other. A mutation that creates orders answers with a `userErrors` entry whose `field` names the value (`placed_at`, or `rows.<n>.placed_at` for one row of several) and one of these codes:

| Code | Meaning | How to fix it |
|---|---|---|
| `placed_at_required` | No `placed_at` was given, or it is empty. | Send when the order was placed. |
| `placed_at_invalid` | The value is not an ISO 8601 date and time with an explicit offset. A time with no offset (`2026-01-20 13:00:05`), a bare date or a date that does not exist is refused, because it would be read in a different time zone on a different server. | Send `2026-01-20T13:00:05Z`, or the local time with its offset: `2026-01-20T14:00:05+01:00`. |
| `placed_at_in_future` | Later than now by more than five minutes. | Send the moment the order was placed, not a date it is due. |
| `placed_at_too_old` | Before 2000-01-01. | Check the value. An epoch read in the wrong unit lands in 1970. |

Orders from a connected store follow the same rule. A [Shopify](https://docs.missin.co.uk/integrations/shopify.md) order is placed at its `processedAt`, the date Shopify shows on the order and uses in its own reports. A [Stripe](https://docs.missin.co.uk/integrations/stripe.md) sale is placed when its invoice was finalized or, for a Checkout payment, when its PaymentIntent was created. A store order without that date is not stored, and the sync records it as failed.

## An order or checkout in a currency MISSIN does not recognise

Every amount in MISSIN is in a currency it knows: one of the active ISO 4217 currencies. An order or checkout whose currency is missing, malformed or not on that list is refused whole: nothing is written, and MISSIN never stores it under another currency, not your store's, not your workspace's. The `userErrors` entry names the field (`currency`, or `currency_code` for a checkout) and one of these codes:

| Code | Meaning | How to fix it |
|---|---|---|
| `currency_required` | No currency was given. | Send the three-letter code of the currency the amounts are in. |
| `currency_invalid` | Not a three-letter code: a symbol (`£`), a name (`POUND`) or a numeric code (`826`). | Send the alphabetic ISO 4217 code, e.g. `GBP`. Case does not matter. |
| `currency_unknown` | Three letters, but not an active ISO 4217 currency: a withdrawn currency (`ANG`, replaced by `XCG`), a fund or metal code (`USN`, `XAU`), or a typo (`GPB`). | Send the currency's current ISO 4217 code. |

Orders and checkouts from a connected store follow the same rule. A Shopify order is in its shop currency (`shopMoney`), a Stripe sale in the currency Stripe settled it in, or charged it in when there was no conversion. A store order or checkout in a currency that is not on the list is not stored, and the sync records it as failed. An imported order is in your workspace's currency, and each of its amounts carries no more decimals than that currency has (two for GBP, none for JPY, three for KWD); a row with more is refused with `invalid_amount` on that row's field, never rounded.

## An amount too large to store

Every amount MISSIN stores holds up to fifteen digits before the decimal point, in the currency's own unit. An order, checkout or imported row with an amount past that is refused whole with `amount_out_of_range`, and the `userErrors` entry's `field` names the amount (`rows.<n>.total_price` on an import). A store order or checkout over the limit is not stored, and the sync records it as failed while the rest of the sync lands. A mutation that hits a database range limit anywhere else answers `value_out_of_range`.

| Code | Meaning | How to fix it |
|---|---|---|
| `amount_out_of_range` | The amount has more than fifteen digits before the decimal point. | Check the value is in the currency's major unit, not its minor unit read twice. |
| `value_out_of_range` | A value is past a database range limit. | Check the values sent; a number in the wrong unit is the usual cause. |

## A checkout without a start time

A checkout is stored only with the moment its basket started, as the store reports it: Shopify's `created_at`, or the Stripe Checkout Session's `created` (for a session reopened through a recovery link, the original session's). A checkout the store gives no start for is refused with `checkout_started_at_required`, or `checkout_started_at_invalid` when the start is not an ISO 8601 date and time with an offset, and the sync records it as failed. MISSIN never dates a checkout by the time it received it. The same holds for the time the store last updated it (Shopify's `updated_at`): a checkout without one is refused with `checkout_updated_at_required`, or `checkout_updated_at_invalid` when it is not an ISO 8601 date and time with an offset.

## On the MCP server

The MCP server at `https://api.missin.co.uk/mcp` takes a secret key as a bearer token only, and refuses a key with the same codes: a missing key, or `x-api-key` on its own, gets `401`, and a publishable key gets `403` with `secret_key_required`. It answers `403` with `insufficient_scope` when a tool needs a higher access level than the key has. Its status codes and limits are on the [tool registry](https://developer.missin.co.uk/mcp/tools.md).

Source: https://developer.missin.co.uk/get-started/errors
