External entitlements
Sell through Arcalotl and grant access in an operator-controlled app using member entitlement webhooks.
Advanced integration
You do not need external entitlements when Arcalotl should manage membership access on a connected community platform such as Discord or Stoat. Configure an entitlement's platform benefits and Arcalotl will grant them automatically. Continue with this guide only when your own application needs to receive and enforce Arcalotl entitlement state.
Use external entitlements when Arcalotl should own checkout and billing, but your application, rather than a connected community platform, should enforce access. The recommended integration has three parts:
- Identify the buyer with your own user ID when you create checkout.
- Store the returned Arcalotl
member_idbeside that user ID. - Consume the member entitlement webhooks and apply their logical state to your own authorization system.
This keeps billing lifecycle policy in Arcalotl and leaves your application in control of its own sessions, permissions, licenses, or feature flags.
Store an identity map
Keep a mapping like this in your database:
| Your field | Example |
|---|---|
external_user_id | usr_7f921 |
arcalotl_member_id | mem_01JMZX1Z7D |
Your user ID is not replaced by Arcalotl's ID. Send it as a namespaced
external subject, then persist the canonical member_id returned by checkout.
A namespace prevents identifiers from different environments or applications
from colliding. For example, use production and staging, or a stable
application slug.
Treat member_id as the durable join key for subsequent API reads and webhook
processing. Keep external_user_id as your own system's key.
Create an external checkout
Call POST /v1/checkout-links with scope checkout:write:
POST /v1/checkout-links
Authorization: Bearer arclt_live_...
Content-Type: application/json
Idempotency-Key: checkout-order-8Z4P1
{
"tier_id": "tier_abc123",
"plan_id": "plan_monthly",
"subject": {
"external": {
"namespace": "production",
"external_user_id": "usr_7f921"
}
},
"client_reference_id": "order_8Z4P1",
"return_url": "https://community.example.com/billing/complete"
}The response identifies both the checkout and the member:
{
"checkout_id": "cs_01JMZX9E7P",
"member_id": "mem_01JMZX1Z7D",
"url": "https://checkout.stripe.com/c/pay/cs_...",
"expires_at": "2026-08-01T12:00:00Z"
}Store member_id before redirecting the buyer to url. External checkout is
provider-hosted and does not require the buyer to have a Discord or other
first-party platform identity. client_reference_id is copied to the
entitlement source so you can correlate a later webhook with your order.
If you already know the Arcalotl member, send
"subject": {"member_id": "mem_..."} instead. Send exactly one subject form.
Handle an abandoned or closed checkout
Creating checkout may create the member and external-subject mapping, but it does not grant an entitlement. If the buyer closes the tab, cancels on the provider page, or lets the session expire:
- no entitlement becomes active;
- no
member.entitlement.grantedevent is emitted; - no platform link code is created automatically;
- the member mapping remains safe to reuse.
When the buyer returns, create a new checkout link. Use a new
Idempotency-Key for a genuinely new attempt. Do not infer payment from the
return URL: show a pending state and wait for the entitlement webhook or
re-fetch the member's entitlements.
Consume logical entitlement events
Create a webhook endpoint subscribed to all three events:
member.entitlement.granted: the effective edge became active;member.entitlement.updated: its sources, benefits, or linked identities changed while the effective status stayed the same;member.entitlement.revoked: the effective edge became inactive.
These events come from Arcalotl's authoritative entitlement ledger. They describe logical access independently of whether a Discord role or another first-party platform action succeeded. That makes them appropriate for an operator-controlled authorization system.
{
"id": "evt_9f2c4b1e5a7d4c0f8b3a2d1e6f5c4b3a",
"type": "member.entitlement.granted",
"timestamp": "2026-08-01T12:34:56Z",
"api_version": "2026-08",
"data": {
"member": {
"id": "mem_01JMZX1Z7D",
"identities": [],
"external_subjects": [
{
"namespace": "production",
"external_user_id": "usr_7f921"
}
]
},
"entitlement": {
"id": "ment_01JMZX7Q2R",
"entitlement_id": "ent_01JMZX7P4C",
"key": "vip-access",
"name": "VIP access",
"revision": 4,
"status": "active",
"sources": [
{
"type": "subscription",
"id": "sub_01JMZX47H8",
"status": "active",
"tier_id": "tier_abc123",
"plan_id": "plan_monthly",
"client_reference_id": "order_8Z4P1"
}
]
},
"changes": {
"added_benefits": [],
"removed_benefits": []
},
"reason": "activation",
"trigger_event_id": "evt_internal_..."
}
}Use data.entitlement.key as the stable operator-facing permission key. Use
the effective status to decide access; do not reconstruct it from individual
subscription or purchase events. Several sources may support one entitlement,
so one source ending can produce an updated event while access remains
active.
Make the consumer idempotent and ordered
Webhook delivery is at least once and retries may arrive out of order. In one database transaction:
- Verify the Standard Webhooks signature against the raw body.
- Ignore an envelope
idyou have already processed. - Find your user by
member.idor the stored external subject. - Lock the row for
(member.id, entitlement.id). - Ignore a revision less than or equal to the revision you stored.
- Upsert
status,key,revision, and any source metadata you need. - Mark the envelope
idprocessed and commit. - Return a
2xxquickly.
For granted and active updated events, enable the permission represented by
key. For revoked, disable it. If you model permissions separately, use
changes.added_benefits and changes.removed_benefits to reconcile those
definitions without changing the effective edge rule.
See Webhooks for signature verification, retry timing, delivery inspection, and redelivery.
Reconcile after downtime
Webhooks are signals, not your only recovery mechanism. Periodically or after an outage:
- Read
GET /v1/events?type=member.entitlement.granted,member.entitlement.updated,member.entitlement.revokedfrom your last cursor, or list recent events. - For any affected member, call
GET /v1/members/{member_id}/entitlements. - Replace your stored effective entitlement rows with the returned state.
The member-entitlements endpoint reads the same authoritative ledger that produces webhook events.
Let a buyer link a first-party platform later
External checkout does not require a platform account. If the buyer later
wants their existing purchase applied to a supported first-party platform,
create a short-lived link code with scope members:write:
POST /v1/members/mem_01JMZX1Z7D/link-codes
Authorization: Bearer arclt_live_...
Content-Type: application/json
{
"intended_platform": "discord"
}Display the returned code and expiry in your own UI. The buyer redeems it with that platform's link command. A code is one-time, expires after 10 minutes, and the newest code for the same member and platform invalidates the previous one.
If the buyer closes your UI or the code expires, nothing happens to checkout,
the member, or their entitlements. Call the same endpoint again to generate a
fresh code. Do not store the code as identity state; store the resulting
member_id mapping and linked identity returned by the member API.
Checklist
- Use a stable, environment-specific external-subject namespace.
- Persist
external_user_idtomember_idbefore redirecting to checkout. - Subscribe to granted, updated, and revoked entitlement events.
- Verify signatures, deduplicate envelope IDs, and enforce increasing entitlement revisions.
- Treat return URLs as navigation, not payment confirmation.
- Reconcile through the member-entitlements endpoint after downtime.
- Generate link codes only when a buyer asks to connect a first-party platform.