ArcalotlArcalotl

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:

  1. Identify the buyer with your own user ID when you create checkout.
  2. Store the returned Arcalotl member_id beside that user ID.
  3. 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 fieldExample
external_user_idusr_7f921
arcalotl_member_idmem_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.granted event 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:

  1. Verify the Standard Webhooks signature against the raw body.
  2. Ignore an envelope id you have already processed.
  3. Find your user by member.id or the stored external subject.
  4. Lock the row for (member.id, entitlement.id).
  5. Ignore a revision less than or equal to the revision you stored.
  6. Upsert status, key, revision, and any source metadata you need.
  7. Mark the envelope id processed and commit.
  8. Return a 2xx quickly.

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:

  1. Read GET /v1/events?type=member.entitlement.granted,member.entitlement.updated,member.entitlement.revoked from your last cursor, or list recent events.
  2. For any affected member, call GET /v1/members/{member_id}/entitlements.
  3. Replace your stored effective entitlement rows with the returned state.

The member-entitlements endpoint reads the same authoritative ledger that produces webhook events.

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_id to member_id before 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.

On this page