Skip to content
Insy
For developers
Esc
navigateopen⌘Jpreview
On this page

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 externalOrderId with a different email or telegramUserId. 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.

Next

Was this page helpful?