Skip to main content

Entitlement Grant Webhook Events

These events fire whenever a customer’s entitlement grant changes state, for example when a license key is generated, a Discord role is assigned, a download link is provisioned, or access is revoked. Subscribe to these events to keep your application in sync with what each customer can access.
EventDescription
entitlement_grant.createdA new grant row was created. Status is delivered immediately for auto-fulfilled license keys, and pending for manually-fulfilled license keys and every other integration.
entitlement_grant.deliveredThe grant transitions to delivered. The customer now has access to the entitled platform, file, or license key.
entitlement_grant.failedDelivery failed and is not being retried. Inspect error_code and error_message.
entitlement_grant.revokedAccess was withdrawn. Inspect revocation_reason to understand why.
All four events share the same EntitlementGrantResponse payload documented in the schema below.

Event Triggers

entitlement_grant.created

A grant row was just inserted. The grant always has a stable id from this point on, even if its status changes. Use this event to record that fulfillment is in progress. For auto-fulfilled license keys the row is inserted directly with status: "delivered" and delivered_at populated, so a single created event is followed by no further state changes unless the grant is later revoked. For manually-fulfilled license keys (entitlements with fulfillment_mode: manual) the row arrives with status: "pending" and no license_key object — there is no key yet. This event is your signal that a key is awaiting fulfillment; supply it via POST /grants/{grant_id}/license-key, which then fires entitlement_grant.delivered. See Manual Fulfillment. For every other integration the row arrives with status: "pending". A delivered or failed event follows once delivery completes:
  • OAuth-based integrations (Discord, GitHub, Notion) include an oauth_url the customer must visit to complete consent. The grant stays pending until the customer authorizes.
  • Platform-direct integrations (Telegram, Framer, Digital Files) sit in pending only briefly while the platform call runs, then move to delivered.

entitlement_grant.delivered

The grant transitioned from pending to delivered. The customer now has the access described by the entitlement. Use this event to unlock dependent features in your own systems, for example to provision a workspace, send a custom welcome email, or mark a “fulfilled” flag. The payload’s delivered_at field captures when delivery completed. For grants that arrived delivered on creation, you’ll receive created and delivered events back to back.

entitlement_grant.failed

Delivery was attempted and failed with a non-retryable error. The error_code and error_message fields explain the failure. Common causes include a revoked OAuth token, a denied platform permission, or a missing target (e.g., a deleted Discord guild).
Treat entitlement_grant.failed as actionable. The customer paid but did not get access. Surface failures to your support team or trigger a regrant once the underlying issue is resolved.

entitlement_grant.revoked

Access was withdrawn at the platform level: Discord role removed, GitHub collaborator removed, license key disabled, file download URLs no longer issued. The revocation_reason field records the trigger.
revocation_reasonTrigger
subscription_cancelledThe customer’s subscription was cancelled (subscription.cancelled event).
subscription_on_holdThe subscription is on hold due to failed renewal (subscription.on_hold). Recoverable: a successful retry produces a re-grant.
subscription_expiredThe subscription reached the end of its term (subscription.expired).
plan_changedThe plan changed; old grants are revoked before new ones are issued (subscription.plan_changed).
refundA refund was processed for the original one-time payment (refund.succeeded).
manualA merchant revoked the grant via the API or dashboard. Manual revokes are not auto-regranted on subscription renewal.
license_key_disabledThe license key behind a license-key grant was disabled. The grant is re-activated automatically if the key is re-enabled.
platform_externalThe platform side of an integration drifted out of sync (for example, a Discord role was removed manually, the GitHub App lost repository access, or a reconciliation pass detected a missing target). The grant is not auto-regranted on subscription renewal until the underlying platform issue is resolved.

Payload Variants

The data field is always an EntitlementGrantResponse object. The payload carries an integration_type field (for example license_key, digital_files, discord) so you can recognize the grant type directly. Two integration types also attach extra nested objects:
  • license_key is included when integration_type is license_key and a key has been issued. It contains the generated key, expiry, and activation usage. For a manually-fulfilled grant still in pending, this object is null until you fulfill the grant.
  • digital_product_delivery is included when integration_type is digital_files. It contains presigned download URLs, the optional instructions, and the optional external_url.
For all other integration types (Discord, GitHub, Telegram, Framer, Notion) both nested fields are null; the relevant configuration is captured in the entitlement itself, not the grant.

Sample Payloads

License key delivered (entitlement_grant.delivered)

{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.delivered",
  "timestamp": "2026-05-01T10:25:33.000000Z",
  "data": {
    "id": "grant_8VbC6JDZzPEqfBPUdpj0K",
    "business_id": "bus_H4ekzPSlcg",
    "entitlement_id": "ent_9xY2bKwQn5MjRpL8d",
    "customer_id": "cus_abc123",
    "external_id": "lk_AAA111BBB222",
    "payment_id": "pay_a1b2c3d4",
    "subscription_id": null,
    "status": "delivered",
    "integration_type": "license_key",
    "license_key": {
      "key": "PRO-AAAA-BBBB-CCCC-DDDD",
      "expires_at": "2027-05-01T00:00:00Z",
      "activations_used": 0,
      "activations_limit": 5
    },
    "digital_product_delivery": null,
    "delivered_at": "2026-05-01T10:25:33Z",
    "revoked_at": null,
    "revocation_reason": null,
    "error_code": null,
    "error_message": null,
    "oauth_url": null,
    "oauth_expires_at": null,
    "metadata": null,
    "created_at": "2026-05-01T10:25:33Z",
    "updated_at": "2026-05-01T10:25:33Z"
  }
}

License key pending manual fulfillment (entitlement_grant.created)

Fired when a customer buys a product whose License Key entitlement uses fulfillment_mode: manual. The grant is pending with no license_key object yet — the merchant must supply the key.
{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.created",
  "timestamp": "2026-05-01T10:24:00.000000Z",
  "data": {
    "id": "grant_8VbC6JDZzPEqfBPUdpj0K",
    "business_id": "bus_H4ekzPSlcg",
    "entitlement_id": "ent_9xY2bKwQn5MjRpL8d",
    "customer_id": "cus_abc123",
    "external_id": null,
    "payment_id": "pay_a1b2c3d4",
    "subscription_id": null,
    "status": "pending",
    "integration_type": "license_key",
    "license_key": null,
    "digital_product_delivery": null,
    "delivered_at": null,
    "revoked_at": null,
    "revocation_reason": null,
    "error_code": null,
    "error_message": null,
    "oauth_url": null,
    "oauth_expires_at": null,
    "metadata": null,
    "created_at": "2026-05-01T10:24:00Z",
    "updated_at": "2026-05-01T10:24:00Z"
  }
}

Digital files delivered (entitlement_grant.delivered)

{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.delivered",
  "timestamp": "2026-05-01T10:30:12.000000Z",
  "data": {
    "id": "grant_2P9rQwYvMxTnKoCb4",
    "business_id": "bus_H4ekzPSlcg",
    "entitlement_id": "ent_files_J3kLmN4oP5",
    "customer_id": "cus_abc123",
    "external_id": "pay_a1b2c3d4",
    "payment_id": "pay_a1b2c3d4",
    "subscription_id": null,
    "status": "delivered",
    "integration_type": "digital_files",
    "license_key": null,
    "digital_product_delivery": {
      "files": [
        {
          "file_id": "df_a4f6c1de",
          "download_url": "https://files.dodopayments.com/.../pro-bundle.zip?Signature=...",
          "filename": "pro-bundle.zip",
          "content_type": "application/zip",
          "file_size": 18742390,
          "expires_in": 900
        }
      ],
      "instructions": "Unzip and run setup.sh from the project root.",
      "external_url": null
    },
    "delivered_at": "2026-05-01T10:30:12Z",
    "revoked_at": null,
    "revocation_reason": null,
    "error_code": null,
    "error_message": null,
    "oauth_url": null,
    "oauth_expires_at": null,
    "metadata": null,
    "created_at": "2026-05-01T10:30:12Z",
    "updated_at": "2026-05-01T10:30:12Z"
  }
}

Discord role created and pending (entitlement_grant.created)

{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.created",
  "timestamp": "2026-05-01T10:31:00.000000Z",
  "data": {
    "id": "grant_DiscordPending5L",
    "business_id": "bus_H4ekzPSlcg",
    "entitlement_id": "ent_discord_patrons",
    "customer_id": "cus_abc123",
    "external_id": "sub_pro_monthly_001",
    "payment_id": null,
    "subscription_id": "sub_pro_monthly_001",
    "status": "pending",
    "integration_type": "discord",
    "license_key": null,
    "digital_product_delivery": null,
    "delivered_at": null,
    "revoked_at": null,
    "revocation_reason": null,
    "error_code": null,
    "error_message": null,
    "oauth_url": "https://discord.com/oauth2/authorize?...",
    "oauth_expires_at": "2026-05-08T10:31:00Z",
    "metadata": null,
    "created_at": "2026-05-01T10:31:00Z",
    "updated_at": "2026-05-01T10:31:00Z"
  }
}

Grant revoked on subscription cancellation (entitlement_grant.revoked)

{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.revoked",
  "timestamp": "2026-06-15T08:12:44.000000Z",
  "data": {
    "id": "grant_8VbC6JDZzPEqfBPUdpj0K",
    "business_id": "bus_H4ekzPSlcg",
    "entitlement_id": "ent_9xY2bKwQn5MjRpL8d",
    "customer_id": "cus_abc123",
    "external_id": "sub_pro_monthly_001",
    "payment_id": null,
    "subscription_id": "sub_pro_monthly_001",
    "status": "revoked",
    "integration_type": "license_key",
    "revocation_reason": "subscription_cancelled",
    "license_key": {
      "key": "PRO-AAAA-BBBB-CCCC-DDDD",
      "expires_at": null,
      "activations_used": 1,
      "activations_limit": 5
    },
    "digital_product_delivery": null,
    "delivered_at": "2026-05-01T10:25:33Z",
    "revoked_at": "2026-06-15T08:12:44Z",
    "error_code": null,
    "error_message": null,
    "oauth_url": null,
    "oauth_expires_at": null,
    "metadata": null,
    "created_at": "2026-05-01T10:25:33Z",
    "updated_at": "2026-06-15T08:12:44Z"
  }
}

Delivery failed (entitlement_grant.failed)

{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.failed",
  "timestamp": "2026-05-01T10:36:21.000000Z",
  "data": {
    "id": "grant_GhFailed7Z",
    "business_id": "bus_H4ekzPSlcg",
    "entitlement_id": "ent_github_repo",
    "customer_id": "cus_abc123",
    "external_id": "pay_a1b2c3d4",
    "payment_id": "pay_a1b2c3d4",
    "subscription_id": null,
    "status": "failed",
    "integration_type": "github",
    "license_key": null,
    "digital_product_delivery": null,
    "delivered_at": null,
    "revoked_at": null,
    "revocation_reason": null,
    "error_code": "github_permission_denied",
    "error_message": "Repository access could not be granted: the GitHub App installation no longer has permission on this repository.",
    "oauth_url": null,
    "oauth_expires_at": null,
    "metadata": null,
    "created_at": "2026-05-01T10:36:00Z",
    "updated_at": "2026-05-01T10:36:21Z"
  }
}

Integration Tips

  • Wait for entitlement_grant.delivered before unlocking dependent features. A payment.succeeded event tells you the money cleared; it does not tell you the customer has the GitHub repo or the Discord role yet. The delivered event is the source of truth for fulfillment.
  • Map revocation_reason to retention flows. A subscription_on_hold revoke usually means the customer’s card failed and the next renewal will re-grant access. A manual or subscription_cancelled revoke is intentional. Treat them differently in customer messaging.
  • Use the grant id as your idempotency key. A single grant emits at most one created event and at most one terminal event (delivered or failed), and at most one revoked event. Re-deliveries from the webhook system can repeat events; dedupe on the grant id plus type.
  • Read integration_type to recognize the grant type. The payload carries integration_type directly (for example license_key, digital_files, discord). The license_key and digital_product_delivery nested objects are populated once their respective grants are delivered; a manually-fulfilled license-key grant stays pending with integration_type: "license_key" and a null license_key until you fulfill it.
  • For OAuth-based grants, surface oauth_url to the customer. The entitlement_grant.created event for Discord, GitHub, or Notion subscriber flows includes an oauth_url and oauth_expires_at. Email it to the customer or display it in your app to unblock delivery.

Detailed view of a single entitlement grant: who it's for, its lifecycle state, and any integration-specific delivery payload.

brand_id
string
required

Brand id this grant belongs to.

business_id
string
required

Identifier of the business that owns the grant.

created_at
string<date-time>
required

Timestamp when the grant was created.

customer_id
string
required

Identifier of the customer the grant was issued to.

entitlement_id
string
required

Identifier of the entitlement this grant was issued from.

id
string
required

Unique identifier of the grant.

integration_type
enum<string>
required

The integration type of the grant's entitlement (e.g. license_key).

Available options:
discord,
telegram,
github,
figma,
framer,
notion,
digital_files,
license_key
metadata
object
required

Arbitrary key-value metadata recorded on the grant.

status
enum<string>
required

Lifecycle status of the grant.

Available options:
Pending,
Delivered,
Failed,
Revoked
updated_at
string<date-time>
required

Timestamp when the grant was last modified.

delivered_at
string<date-time> | null

Timestamp when the grant transitioned to delivered, when applicable.

digital_product_delivery
Digital Product Delivery · object

Digital-product-delivery payload, present when the entitlement integration is digital_files.

error_code
string | null

Machine-readable code reported when delivery failed, when applicable.

error_message
string | null

Human-readable message reported when delivery failed, when applicable.

license_key
object

License-key delivery payload, present when the entitlement integration is license_key.

oauth_expires_at
string<date-time> | null

Timestamp when oauth_url stops being valid, when applicable.

oauth_url
string | null

Customer-facing OAuth URL for OAuth-style integrations. Populated during the customer-portal accept flow; null until the customer completes that step, and on grants for non-OAuth integrations.

payment_id
string | null

Identifier of the payment that triggered this grant, when applicable.

revocation_reason
string | null

Reason recorded when the grant was revoked, when applicable.

revoked_at
string<date-time> | null

Timestamp when the grant transitioned to revoked, when applicable.

subscription_id
string | null

Identifier of the subscription that triggered this grant, when applicable.

Last modified on June 8, 2026