Errors
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:
{
"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:
{
"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.
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 — 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.
401 versus 403
401normally means Insy could not identify you. The key is absent, mistyped, or no longer active. That body carriesstatusCodeand has noerrorCode.403means 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 thecommunityIdin 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.
{
"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.
Common causes
401 on every request, but the key looks rightHeader 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.
403 even though the key has the right permissionCommunity 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.
400 on provision-externalIdentifying 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.
The same buyer was provisioned twiceIdempotency 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.
buy-plan did not return a checkoutUrlRequired 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.
404 for an ID that exists in your dashboardWrong 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 and call again.
The Telegram invite link no longer works24-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.
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.