> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dodopayments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Entitlement Grant

> The payload sent to your webhook endpoint when an entitlement grant is created, delivered, fails, or is revoked.

## 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.

| Event | Description |
| - | - |
| `entitlement_grant.created` | A new grant row was created. Status is `Delivered` immediately for auto-fulfilled license keys and feature flags, and `Pending` for manually-fulfilled license keys and every other integration. |
| `entitlement_grant.delivered` | The grant transitions to delivered. The customer now has access to the entitled platform, file, or license key. |
| `entitlement_grant.failed` | Delivery failed and is not being retried. Inspect `error_code` and `error_message`. |
| `entitlement_grant.revoked` | Access 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 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** and **feature flags** 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`](/api-reference/entitlements/fulfill-license-key), which then fires `license_key.created` and `entitlement_grant.delivered`. See [Manual Fulfillment](/features/license-keys#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) use an `oauth_url` the customer must visit to complete consent. Dodo Payments tries to create it when the grant is created, so `entitlement_grant.created` may include it; if it is `null`, it is filled in when the customer starts the accept flow from the Customer Portal. 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 to `Delivered`, usually from `Pending`. 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. `delivered` fires whenever an existing grant's status changes to `Delivered`: from `Pending`, when a failed OAuth grant later succeeds, or when a revoked grant is restored. A grant that arrives `Delivered` on creation, such as an auto-fulfilled license key, fires `created` only.

### 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).

<Tip>
  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.
</Tip>

### 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_reason` | Trigger |
| - | - |
| `subscription_cancelled` | The customer's subscription was cancelled (`subscription.cancelled` event). |
| `subscription_on_hold` | The subscription is on hold due to failed renewal (`subscription.on_hold`). Recoverable: a successful retry produces a re-grant. |
| `subscription_paused` | The subscription was paused (`subscription.paused`). Recoverable: resuming produces a re-grant. |
| `subscription_expired` | The subscription reached the end of its term (`subscription.expired`). |
| `plan_changed` | The plan changed; old grants are revoked before new ones are issued (`subscription.plan_changed`). |
| `refund` | A refund was processed for the original one-time payment (`refund.succeeded`). |
| `manual` | A merchant revoked the grant via the API or dashboard. Manual revokes are not auto-regranted on subscription renewal. |
| `license_key_disabled` | The license key behind a license-key grant was disabled. The grant is re-activated automatically if the key is re-enabled. |
| `platform_external` | The 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. Three 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`.
* **`feature`** is included when `integration_type` is `feature_flag`. It contains the `feature_type` and `feature_id` of the capability conferred by the grant.

For all other integration types (Discord, GitHub, Telegram, Figma, Framer, Notion) these nested fields are `null`; the relevant configuration is captured in the entitlement itself, not the grant.

***

## Sample Payloads

### License Key Delivered (`entitlement_grant.delivered`)

```json theme={null} theme={null}
{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.delivered",
  "timestamp": "2026-05-01T10:25:33.000000Z",
  "data": {
    "payload_type": "EntitlementGrant",
    "id": "grant_8VbC6JDZzPEqfBPUdpj0K",
    "business_id": "bus_H4ekzPSlcg",
    "brand_id": "brd_123",
    "entitlement_id": "ent_9xY2bKwQn5MjRpL8d",
    "customer_id": "cus_abc123",
    "payment_id": "pay_a1b2c3d4",
    "subscription_id": null,
    "status": "Delivered",
    "integration_type": "license_key",
    "license_key": {
      "id": "lic_4hQ2mZ8bR",
      "key": "PRO-AAAA-BBBB-CCCC-DDDD",
      "status": "active",
      "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": {},
    "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.

```json theme={null} theme={null}
{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.created",
  "timestamp": "2026-05-01T10:24:00.000000Z",
  "data": {
    "payload_type": "EntitlementGrant",
    "id": "grant_8VbC6JDZzPEqfBPUdpj0K",
    "business_id": "bus_H4ekzPSlcg",
    "brand_id": "brd_123",
    "entitlement_id": "ent_9xY2bKwQn5MjRpL8d",
    "customer_id": "cus_abc123",
    "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": {},
    "created_at": "2026-05-01T10:24:00Z",
    "updated_at": "2026-05-01T10:24:00Z"
  }
}
```

### Digital Files Delivered (`entitlement_grant.delivered`)

```json theme={null} theme={null}
{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.delivered",
  "timestamp": "2026-05-01T10:30:12.000000Z",
  "data": {
    "payload_type": "EntitlementGrant",
    "id": "grant_2P9rQwYvMxTnKoCb4",
    "business_id": "bus_H4ekzPSlcg",
    "brand_id": "brd_123",
    "entitlement_id": "ent_files_J3kLmN4oP5",
    "customer_id": "cus_abc123",
    "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": {},
    "created_at": "2026-05-01T10:30:12Z",
    "updated_at": "2026-05-01T10:30:12Z"
  }
}
```

### Discord Role Created and Pending (`entitlement_grant.created`)

```json theme={null} theme={null}
{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.created",
  "timestamp": "2026-05-01T10:31:00.000000Z",
  "data": {
    "payload_type": "EntitlementGrant",
    "id": "grant_DiscordPending5L",
    "business_id": "bus_H4ekzPSlcg",
    "brand_id": "brd_123",
    "entitlement_id": "ent_discord_patrons",
    "customer_id": "cus_abc123",
    "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": {},
    "created_at": "2026-05-01T10:31:00Z",
    "updated_at": "2026-05-01T10:31:00Z"
  }
}
```

### Grant Revoked on Subscription Cancellation (`entitlement_grant.revoked`)

```json theme={null} theme={null}
{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.revoked",
  "timestamp": "2026-06-15T08:12:44.000000Z",
  "data": {
    "payload_type": "EntitlementGrant",
    "id": "grant_8VbC6JDZzPEqfBPUdpj0K",
    "business_id": "bus_H4ekzPSlcg",
    "brand_id": "brd_123",
    "entitlement_id": "ent_9xY2bKwQn5MjRpL8d",
    "customer_id": "cus_abc123",
    "payment_id": null,
    "subscription_id": "sub_pro_monthly_001",
    "status": "Revoked",
    "integration_type": "license_key",
    "revocation_reason": "subscription_cancelled",
    "license_key": {
      "id": "lic_4hQ2mZ8bR",
      "key": "PRO-AAAA-BBBB-CCCC-DDDD",
      "status": "disabled",
      "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": {},
    "created_at": "2026-05-01T10:25:33Z",
    "updated_at": "2026-06-15T08:12:44Z"
  }
}
```

### Delivery Failed (`entitlement_grant.failed`)

```json theme={null} theme={null}
{
  "business_id": "bus_H4ekzPSlcg",
  "type": "entitlement_grant.failed",
  "timestamp": "2026-05-01T10:36:21.000000Z",
  "data": {
    "payload_type": "EntitlementGrant",
    "id": "grant_GhFailed7Z",
    "business_id": "bus_H4ekzPSlcg",
    "brand_id": "brd_123",
    "entitlement_id": "ent_github_repo",
    "customer_id": "cus_abc123",
    "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": {},
    "created_at": "2026-05-01T10:36:00Z",
    "updated_at": "2026-05-01T10:36:21Z"
  }
}
```

***

## Integration Tips

* **Unlock dependent features when a grant reaches `Delivered`.** 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. Handle `entitlement_grant.delivered`, and also `entitlement_grant.created` with `status: "Delivered"`, because a grant that is delivered on creation fires no `delivered` event.
* **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.
* **Detect duplicates with the `webhook-id` header, not the grant `id`.** A grant emits `created` once, but `delivered` and `revoked` can each fire more than once, because a revoked grant can be restored and revoked again. `failed` is not always final either: a failed OAuth grant can still be delivered. Re-deliveries from the webhook system can also repeat an event. Skip repeats by `webhook-id`, and key your own grant records on the grant `id`.
* **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 may include an `oauth_url` and `oauth_expires_at`. If it is `null`, wait for a later event or direct the customer to the Customer Portal. Email the URL to the customer or display it in your app to unblock delivery.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.