---
title: Errors
description: HTTP status codes the Insy REST API returns, the two failure body shapes, and the usual causes behind 400, 401, 403, 404 and 409 responses.
---

Failures come back in one of two body shapes, depending on where in the request the failure
happened. Branch on the HTTP status first, then read whichever body you got. `success` is
present in both.

## Rejected before the endpoint runs

A key that fails authentication, a body the validator refuses, a permission bit or community
role that says no — these are raised before the endpoint's own code runs and are serialised
by the global exception filter:

```json title="Rejected request"
{
  "success": false,
  "message": "Unauthorized",
  "statusCode": 401,
  "timestamp": "2026-09-01T10:15:30.000Z",
  "path": "/membership/provision-external"
}
```

There is no `errorCode` and no `data` key at all. This is the shape behind almost every
`400`, `401` and `403`.

## Returned by the endpoint

Failures the endpoint decides on itself come back in the `BaseResponse` envelope:

```json title="Failure envelope"
{
  "success": false,
  "errorCode": "not_found",
  "message": "Community not found",
  "correlationKey": "0e9a1c47f2b84d6a",
  "logUrl": "https://observability.insy.io/explore?..."
}
```

| Field | Type | Description |
| --- | --- | --- |
| `success` | boolean | Always `false` on failure. |
| `errorCode` | string | A short machine-readable code, for example `not_found`. |
| `message` | string | A human-readable explanation, safe to log. |
| `correlationKey` | string | The trace ID for the request. Quote it when you report a problem. |
| `logUrl` | string | An Insy-internal log link for the same trace. You cannot open it; pass it on unchanged. |

`data` is omitted rather than sent as `null`. `correlationKey` and `logUrl` are present only
when the request carried a trace ID, so treat both as optional.

> **Treat errorCode as opaque**
>
> Codes are short lowercase snake_case strings — `not_found`, `bad_request`, `unauthorized`,
> `forbidden`, `already_exists`, `internal_server_error` among others — and the set grows as
> endpoints are added. A comparison against `"NOT_FOUND"` never matches. Do not build control
> flow on an exhaustive list either: switch on the HTTP status, and keep `errorCode` and
> `message` for logs and support tickets.

Do not show `message` to end users verbatim. It is written for developers and can name
internal concepts.

## Status codes

| Status | Meaning | What to do |
| --- | --- | --- |
| `400` | Validation failed. A required field is missing, malformed, or outside its allowed values. | Fix the request. Retrying the same body will fail again. |
| `401` | The API key is missing, malformed, unknown or revoked. | Check the `Authorization` header and the key itself. Do not retry. |
| `403` | Authenticated, but not allowed. The permission bit is missing, or the key owner is not OWNER or ADMIN of the community. | Issue a new key with that permission at [Account → Developer → API keys](https://insy.io/account/developer/api-keys) — permissions cannot be edited after creation — or point the call at a community the owner administers. Do not retry. |
| `404` | The addressed resource does not exist — a community or reservation that is not there, or a checkout whose payment has not completed yet. | Verify the IDs. Do not retry. |
| `409` | The membership already exists. `onOverlap: "error"` hit an active membership, or the membership is managed by a Stripe subscription and cannot be provisioned externally. Also returned when the same address is signed up twice for one drop. | Send `extend` or `replace`, or leave the Stripe subscription alone. Do not retry. |
| `500` | Something failed on Insy's side. | Retry with backoff. If it persists, contact Insy with the `correlationKey`, the `message` and the time of the call. |

The same mapping can also produce `412`, `422`, `503` and `504`. If you meet a status that is
not in the table, treat it by its class: 4xx means change the request, 5xx means retry with
backoff. API-key requests are not rate limited, so there is no `429` or retry-after to
handle — but avoid tight polling anyway.

> **Some failures arrive with a 2xx**
>
> Two paths report failure inside a successful-looking response.
> `POST /api/checkout/buy-plan` answers `201` for the failures its own logic reports (missing
> email, unknown plan): an unknown `pricingPlanId` gives `"success": true` with an empty
> `data`, and a missing `email` gives `"success": false` with no `errorCode`. Malformed or
> unauthorized requests still return `400`, `401` or `403` — the `201` applies only once the
> request has passed validation and auth. `GET /membership/join-telegram-community-external/{externalId}`
> answers `200` with `"success": false` and the message `Telegram bot not setup` when the
> community has no bot. Check that the field you needed is actually present rather than trusting
> the status code.

> **Retries are safe on provision-external**
>
> `POST /membership/provision-external` is idempotent on `externalOrderId`, so an immediate
> retry after a `500` or a network timeout cannot provision twice. Reuse the same
> `externalOrderId`, and retry before you provision another order for the same buyer — the
> guarantee only holds against that buyer's most recent provision. See
> [Endpoints](/developers/api/endpoints).

### 401 versus 403

- `401` normally means Insy could not identify you. The key is absent, mistyped, or no
  longer active. That body carries `statusCode` and has no `errorCode`.
- `403` means Insy knows who you are and is refusing. Either the key lacks the permission
  bit for that endpoint, or the key's owner is not an OWNER or ADMIN of the `communityId` in
  the request.

A key that works against one community and returns `403` against another is almost always
the second case.

One endpoint breaks the `401` rule.
`GET /membership/join-telegram-community-external/{externalId}` answers `401` with
`"errorCode": "unauthorized"` and the message "You are not a member of this community" when
the buyer behind that checkout has no active membership. The key is fine; the membership is
not. Tell the two apart by the presence of `errorCode` — a rejected key never has one.

## OAuth errors are shaped differently

The OAuth token, revoke, userinfo, memberships and products endpoints do not use the
`BaseResponse` envelope. They return bare RFC-shaped JSON.

```json title="OAuth error"
{
  "error": "invalid_grant",
  "error_description": "..."
}
```

`invalid_client` is returned with HTTP `401`; every other token error is `400`. Resource
endpoints return `401` with `"error": "invalid_token"` and `403` with
`"error": "insufficient_scope"`. No `WWW-Authenticate` response header is sent, so a client
library that waits for one to decide how to re-authenticate will not get the hint. See
[Sign in with Insy](/developers/oauth).

## Common causes

<Accordion>
  <AccordionItem title="401 on every request, but the key looks right" description="Header formatting">
    The key goes in `Authorization` as a bearer token: `Authorization: Bearer insy_...`.
    There is no `x-api-key` header. Check that the word `Bearer` and a single space precede
    the key, that nothing has trimmed the `insy_` prefix, and that no newline was copied
    along with it. If the header is correct, the key may have been revoked — revocation
    takes effect immediately.
  </AccordionItem>

  <AccordionItem title="403 even though the key has the right permission" description="Community role">
    Whenever a request carries a `communityId`, the key's owner must additionally hold an
    active OWNER or ADMIN membership in that community. The permission bit and the community
    role are two independent checks, and failing the second produces `403`. Confirm the
    `communityId` is the one the key was issued for, and that the owner's membership in it is
    still active.
  </AccordionItem>

  <AccordionItem title="400 on provision-external" description="Identifying the user">
    Provisioning needs a way to find or create the person: supply `email` or
    `telegramUserId`. Also check that `communityId` and `pricingPlanId` are UUIDs, that
    `validFrom` and `validUntil` are ISO-8601 timestamps, and that `onOverlap` is one of
    `extend`, `replace` or `error`. For a lifetime membership, omit `validUntil` — do not
    send `null`.
  </AccordionItem>

  <AccordionItem title="The same buyer was provisioned twice" description="Idempotency key">
    `externalOrderId` is the idempotency key, and it only protects you if it is stable. A
    value derived from a timestamp, a UUID generated at call time, or a retry counter is a
    different key on every attempt, so each attempt provisions. Derive it from something
    durable on your side, such as your order ID. The check is also scoped: Insy compares the
    ID against the buyer's current membership row for that community only, so replaying an
    old order after a later one for the same buyer, or sending the same ID with a different
    `email` or `telegramUserId`, provisions again. See
    [Idempotency](/developers/api/endpoints#idempotency).
  </AccordionItem>

  <AccordionItem title="buy-plan did not return a checkoutUrl" description="Required fields">
    `pricingPlanId`, `communityId`, `externalId`, `email`, `successUrl` and `cancelUrl` are
    all required. `successUrl` and `cancelUrl` must be valid URLs, `externalId` is your own
    identifier for the buyer rather than an Insy ID, and `utmSource` is capped at 100
    characters. A missing `email` and an unknown `pricingPlanId` both come back as `201`
    with no `checkoutUrl`, so read `message` to tell them apart.
  </AccordionItem>

  <AccordionItem title="404 for an ID that exists in your dashboard" description="Wrong ID or a payment still in flight">
    Ownership is enforced through the `communityId` in the request, so an endpoint that does
    not carry one — `GET /membership/join-telegram-community-external/{externalId}` — is not
    scoped to your communities. A `404` there means the `externalId` is unknown, not that it
    belongs to someone else: check you sent the `externalId` from `buy-plan` and not an
    `externalOrderId` from `provision-external`, which creates no checkout reservation at
    all. The same endpoint returns `404` with "Membership not found, membership has not been
    processed yet" while the buyer's payment is still in flight — wait for the
    [`payment.success` webhook](/developers/webhooks/events) and call again.
  </AccordionItem>

  <AccordionItem title="The Telegram invite link no longer works" description="24-hour expiry, bound to one account">
    Invite links from
    `GET /membership/join-telegram-community-external/{externalId}` expire 24 hours after
    they are minted, and each one is bound to the buyer's Telegram account — a join request
    from a different account is rejected. A link emailed the day before is stale. Fetch a
    fresh link at the moment you show it to the buyer.
  </AccordionItem>
</Accordion>

## Reporting a problem

There is no per-key usage log, so Insy cannot look up your request after the fact from the
key alone. When you report an issue, include the endpoint, the UTC timestamp, the HTTP
status, and the `errorCode` and `message` from the response body.

If the body carried a `correlationKey`, quote it. That is the trace ID Insy can look the
request up by directly, and it turns a report into a single query. Send the `logUrl`
alongside it if one was present.
