Endpoints
Reference for the five Insy REST endpoints an API key can call — membership provisioning, Telegram invites and checks, hosted checkout and drop signups.
An API key can call five endpoints. This is the complete list — nothing else on
https://api.insy.io accepts key authentication.
| Endpoint | Permission |
|---|---|
POST /membership/provision-external |
MEMBERSHIP_WRITE + MEMBERSHIP_PROVISION (private) |
GET /membership/join-telegram-community-external/{externalId} |
MEMBERSHIP_WRITE |
GET /membership/check-telegram/{communityId}/{telegramUserId} |
MEMBERSHIP_READ |
POST /api/checkout/buy-plan |
CHECKOUT_WRITE |
POST /api/drop-notification/signup |
DROP_NOTIFICATION_WRITE |
Every request carries Authorization: Bearer insy_.... Responses use the
BaseResponse envelope, so the payloads below are what you find
under data.
POST /membership/provision-external
Permission: MEMBERSHIP_WRITE and MEMBERSHIP_PROVISION (a privileged,
separately-granted permission), plus OWNER or ADMIN in communityId. See
Privileged permissions.
Provisions a community membership from an external funnel. Insy finds the user by the identifier you supply, creates the user if there is no match, and grants or adjusts their membership in the community.
Use this when the sale happened somewhere else — your own checkout, a course platform, a manual bank transfer — and you want Insy to be the system of record for access.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
communityId |
uuid | yes | The community to provision into. |
externalOrderId |
string | yes | Your identifier for the order. Acts as the idempotency key. |
email |
string | no | The buyer’s email. Supply this or telegramUserId. |
telegramUserId |
integer | no | The buyer’s numeric Telegram user ID. Supply this or email. |
pricingPlanId |
uuid | no | The pricing plan the membership should be attached to. |
validFrom |
ISO-8601 | no | When access starts. Defaults to now, and is ignored on a first provision — see below. |
validUntil |
ISO-8601 | no | When access ends. Omit for a lifetime membership. |
onOverlap |
string | no | extend, replace or error. Defaults to extend. |
Identify the user with email or telegramUserId. If you have both, sending both gives
Insy the best chance of matching an existing person rather than creating a second one.
Idempotency
externalOrderId is the idempotency key, and the guarantee is narrower than the name
suggests. Insy keeps no ledger of the order IDs it has processed. It stores the ID on the
buyer’s membership row for that community and compares against that single value, so a
replay is recognised only while it is still the most recent provision for the same buyer in
the same community. When it is recognised, the existing membership is returned untouched
with status set to idempotent_return.
If your request times out, your webhook handler runs twice, or a queue redelivers a job,
send exactly the same externalOrderId again and the buyer ends up with one membership.
Two replays are not protected, and both provision again:
- Replaying order A after you have provisioned order B for the same buyer and community. The stored ID is now B’s, so A no longer matches.
- Sending the same
externalOrderIdwith a differentemailortelegramUserId. That resolves to a different buyer, whose membership row has never seen the ID.
Overlap strategies
onOverlap decides what happens when the user already has a membership in this community
that overlaps the window you are asking for.
| Value | Behaviour |
|---|---|
extend |
Default. Keeps whichever window ends later — your validUntil or the existing one. If either side is open-ended the membership becomes lifetime. |
replace |
Discards the existing window and applies yours. |
error |
Refuses the request with 409 rather than touching the existing membership. |
extend does not add your period to the time remaining. Send the absolute end date you
want, not a duration to add: a validUntil 30 days out for a member with 20 days left
leaves that member with 30 days in total, not 50. Compute the new end date from the existing
one on your side if you need the two to accumulate.
onOverlap only comes into play when the existing membership is still active. An expired
membership is overwritten with the window you send whichever strategy you choose.
Pick extend for renewals and top-ups, replace when your system is the authority on the
access window, and error when an overlap means something has gone wrong upstream and you
would rather investigate than guess.
Lifetime memberships
Omit validUntil entirely to grant lifetime access. Do not send null or a far-future
date — the absence of the field is the signal.
Result statuses
The response reports what actually happened.
| Status | Meaning |
|---|---|
created |
The buyer had no membership row in this community. |
extended |
A membership row existed and was active. |
replaced |
A membership row existed but had lapsed. |
idempotent_return |
The externalOrderId matches the one stored on the buyer’s membership; it is returned unchanged. |
status describes the state of the buyer’s membership before the call, not the strategy you
sent. An onOverlap: "replace" request against an active membership still reports
extended, and an onOverlap: "extend" request against a lapsed one reports replaced.
Use it to tell a first purchase from a renewal, not to confirm which strategy ran.
Request
curl -X POST https://api.insy.io/membership/provision-external \
-H "Authorization: Bearer insy_4f3a9c1e7b2d5081a6c4e9f3b7d2508a" \
-H "Content-Type: application/json" \
-d '{
"communityId": "3b7d1f42-9c8a-4e51-b0d6-2a4f8e7c1953",
"externalOrderId": "order_10241",
"email": "buyer@example.com",
"pricingPlanId": "a91f0c46-5d3b-4e88-9a72-1f6b0d4c8e37",
"validFrom": "2026-09-01T00:00:00.000Z",
"validUntil": "2026-10-01T00:00:00.000Z",
"onOverlap": "extend"
}'const provision = async (order) => {
const response = await fetch("https://api.insy.io/membership/provision-external", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INSY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
communityId: "3b7d1f42-9c8a-4e51-b0d6-2a4f8e7c1953",
// Stable per order, so a retry cannot provision twice.
externalOrderId: order.id,
email: order.buyerEmail,
validFrom: order.paidAt,
validUntil: order.accessEndsAt,
onOverlap: "extend",
}),
});
const body = await response.json();
if (!response.ok || !body.success) {
throw new Error(`${body.errorCode ?? response.status}: ${body.message}`);
}
return body.data;
};Response
{
"success": true,
"data": {
"membershipId": "7c1d0a58-4b2e-4f39-8e6a-05c9f2b7d413",
"userId": "e4a2b915-83c7-4d60-9f1e-6b0a37c5d284",
"validFrom": "2026-09-01T00:00:00.000Z",
"validUntil": "2026-10-01T00:00:00.000Z",
"status": "created"
}
}
| Field | Type | Description |
|---|---|---|
membershipId |
uuid | The membership that was created or updated. This is the only handle you ever get on it — store it against your order. |
userId |
uuid | The Insy user the membership belongs to, matched or created from the identifier you sent. |
validFrom |
ISO-8601 | When access actually starts, as stored. On a first provision this is the time of the call, not the validFrom you sent. |
validUntil |
ISO-8601 or null | When access ends. null for a lifetime membership. |
status |
string | What state the membership was in — see Result statuses. |
A recognised repeat of the same externalOrderId returns the same five fields with
"status": "idempotent_return" and the membership exactly as it already stands.
GET /membership/join-telegram-community-external/{externalId}
Permission: MEMBERSHIP_WRITE.
Returns a fresh Telegram invite link for a checkout session you opened with
POST /api/checkout/buy-plan, along with the timestamp at which the link expires.
externalId is the value you sent to buy-plan — not the externalOrderId from
provision-external. Insy resolves the checkout reservation by that value, and the link is
available only once the buyer’s payment has completed and the reservation has been turned
into a membership. Before that you get 404 with “Membership not found, membership has not
been processed yet”. Memberships created with provision-external have no reservation
behind them and cannot be used here at all.
Call this at the moment you are about to show the buyer their invite. The link expires 24 hours after it is minted and is bound to the buyer’s Telegram account — a join request from any other Telegram account is rejected. If more than a day has passed, call the endpoint again for a new one.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
externalId |
path | string | yes | The externalId you sent to POST /api/checkout/buy-plan. |
Request
curl https://api.insy.io/membership/join-telegram-community-external/lead_88213 \
-H "Authorization: Bearer insy_4f3a9c1e7b2d5081a6c4e9f3b7d2508a"
Response
{
"success": true,
"data": {
"link": "https://telegram.me/joinchat/AAAAAE1z7Kq9Rr0pXv3Ttw",
"expiresAt": "2026-09-02T10:00:00.000Z"
}
}
Present link to the buyer immediately and do not cache it past expiresAt — 24 hours
after the call.
GET /membership/check-telegram/{communityId}/{telegramUserId}
Permission: MEMBERSHIP_READ, plus OWNER or ADMIN in communityId.
Reports whether a given Telegram user currently holds an active membership in the community.
Use it to gate something on your own side — a support form, a private feed, a bot command — without mirroring Insy’s membership state into your database.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
communityId |
path | uuid | yes | The community to check. |
telegramUserId |
path | integer | yes | The numeric Telegram user ID. |
Request
curl https://api.insy.io/membership/check-telegram/3b7d1f42-9c8a-4e51-b0d6-2a4f8e7c1953/482910375 \
-H "Authorization: Bearer insy_4f3a9c1e7b2d5081a6c4e9f3b7d2508a"
Response
{
"success": true,
"data": {
"isActive": true,
"membershipId": "7c1d0a58-4b2e-4f39-8e6a-05c9f2b7d413"
}
}
membershipId is null when there is no active membership. This is a point-in-time answer —
a membership that expires a minute later still reads as active. For state changes as they
happen, subscribe to membership webhooks instead of polling.
POST /api/checkout/buy-plan
Permission: CHECKOUT_WRITE, plus OWNER or ADMIN in communityId.
Creates a hosted checkout session and returns the URL to send the buyer to. Insy handles payment, the membership grant and the receipts; you handle the redirect.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
pricingPlanId |
uuid | yes | The plan being bought. |
communityId |
uuid | yes | The community the plan belongs to. |
externalId |
string | yes | Your own identifier for the buyer. Reuse it later to fetch the Telegram invite. |
email |
string | yes | The buyer’s email. Lower-cased and trimmed. |
successUrl |
url | yes | Where to send the buyer after a completed payment. |
cancelUrl |
url | yes | Where to send the buyer if they abandon checkout. |
utmSource |
string | no | Max 100 characters. Shown in Insy purchase reporting. |
email is not a pre-fill. It is the identity the checkout is bound to: Insy resolves it to
an existing user or creates one, and the membership from the completed payment lands on that
account. The published OpenAPI schema marks the field optional, but the endpoint rejects a
body without it.
Set utmSource to the campaign, page or partner that produced the click — it is the only
way to attribute a sale created this way back to its origin in Insy’s purchase reporting.
Request
curl -X POST https://api.insy.io/api/checkout/buy-plan \
-H "Authorization: Bearer insy_4f3a9c1e7b2d5081a6c4e9f3b7d2508a" \
-H "Content-Type: application/json" \
-d '{
"pricingPlanId": "a91f0c46-5d3b-4e88-9a72-1f6b0d4c8e37",
"communityId": "3b7d1f42-9c8a-4e51-b0d6-2a4f8e7c1953",
"externalId": "lead_88213",
"successUrl": "https://shop.example/thanks",
"cancelUrl": "https://shop.example/pricing",
"email": "buyer@example.com",
"utmSource": "newsletter-september"
}'const response = await fetch("https://api.insy.io/api/checkout/buy-plan", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INSY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
pricingPlanId: "a91f0c46-5d3b-4e88-9a72-1f6b0d4c8e37",
communityId: "3b7d1f42-9c8a-4e51-b0d6-2a4f8e7c1953",
externalId: lead.id,
successUrl: "https://shop.example/thanks",
cancelUrl: "https://shop.example/pricing",
email: lead.email,
utmSource: "newsletter-september",
}),
});
const body = await response.json();
// buy-plan answers 201 even when it failed, so the URL is the only signal.
const url = body?.data?.checkoutUrl;
if (!url) {
throw new Error(body?.message ?? "checkout session was not created");
}
// Redirect the buyer to url.Response
{
"success": true,
"data": {
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_live_9d2f7a136c404b8ea1f53e07b9c2d846"
}
}
Redirect the buyer to checkoutUrl. Do not treat the redirect to successUrl as proof of
payment — a buyer can reach that URL by hand. Confirm with the
payment.success webhook.
POST /api/drop-notification/signup
Permission: DROP_NOTIFICATION_WRITE, plus OWNER or ADMIN in communityId.
Signs an email address up for a community’s next upcoming drop slot, so the person is notified when that drop opens. An optional phone number can be captured alongside it.
Use it behind a waitlist form on your own landing page when a community sells in limited drops.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
communityId |
uuid | yes | The community whose next drop the person is joining. |
email |
string | yes | The address to notify. |
phoneNumber |
string | no | An optional phone number for the same signup. |
Request
curl -X POST https://api.insy.io/api/drop-notification/signup \
-H "Authorization: Bearer insy_4f3a9c1e7b2d5081a6c4e9f3b7d2508a" \
-H "Content-Type: application/json" \
-d '{
"communityId": "3b7d1f42-9c8a-4e51-b0d6-2a4f8e7c1953",
"email": "waitlist@example.com",
"phoneNumber": "+48500100200"
}'
Response
{
"success": true,
"data": {
"signedUp": true,
"hasExistingAccount": false,
"dropSlotInfo": {
"id": "5f8c2b41-9a70-4e2d-b3c8-71e0d6a4f925",
"slots": 200,
"slotsUsed": 137,
"availableSlots": 63,
"startsAt": "2026-09-15T18:00:00.000Z",
"endsAt": null,
"isCurrentlyActive": false
}
}
}
| Field | Type | Description |
|---|---|---|
signedUp |
boolean | Whether a signup was recorded. false when the address already has an Insy account. |
hasExistingAccount |
boolean | true when the address was skipped for that reason. |
dropSlotInfo |
object | The drop slot the signup was aimed at, including its start and its remaining slots. slots and availableSlots are null for an unlimited drop. |
dropSlotInfo also carries a dropNotification field (a notification object, or null) and
sometimes a userHasNotification boolean. Both are internal bookkeeping the client can
ignore.
The signup targets the community’s next upcoming drop slot, resolved at the time of the
call. There is no way to name a specific future drop. Signing the same address up twice for
the same drop returns 409 with errorCode already_exists.