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

# Feature Flag Entitlement

> Gate features in your own application off a purchase. Feature flag entitlements deliver a boolean capability on payment and revoke it automatically on cancellation.

<Info>
  A feature flag entitlement turns Dodo Payments into a billing-aware feature flag store. Attach a flag such as `advanced_reports` to a product, and every paying customer gets a grant that your application checks through the API or keeps in sync with webhooks. There's no external platform, OAuth step, or delivery step: the grant itself is the capability.
</Info>

## What Gets Delivered

Nothing leaves Dodo Payments. The grant **is** the deliverable:

* On purchase, Dodo Payments creates the grant directly in `Delivered`. It never enters `Pending`, needs no customer action, and has no delivery step that can fail.
* The grant carries a typed `feature` payload: `{ "feature_type": "boolean", "feature_id": "advanced_reports" }`. Your application reads `feature_id` to decide what to unlock.
* Cancellation, refund, or a manual revoke moves the grant to `Revoked`, and the flag disappears from the customer's delivered grants.

Common uses include plan-based feature gating (Pro unlocks analytics), add-on capabilities (an "API access" upgrade), and early-access programs sold as one-time purchases.

<Note>
  `feature_id` is an identifier you choose, and it isn't unique across entitlements. Two entitlements can confer the same `feature_id`, for example a monthly and a yearly Pro plan that both grant `advanced_reports`.
</Note>

## Create a Feature Flag

<Steps>
  <Step title="Open Entitlements">
    In the Dodo Payments dashboard, go to **Entitlements** and click **+** to start a new entitlement, then choose **Feature Flags**.
  </Step>

  <Step title="Name the Flag">
    Enter a **Display Name** for your dashboard and reports, and a **Description** so your team knows what the flag controls. The **Feature ID** is what your application checks. The dashboard fills it in from the display name (for example, "API access" becomes `api_access`), and you can edit it. It can't contain spaces.

    <Frame caption="Creating a feature flag. The Feature ID is what your application checks; Meta Data attaches limits alongside the flag.">
      <img src="https://mintcdn.com/dodopayments/oS2MTbJuY6MeBjjs/images/entitlements/feature-flags/create.png?fit=max&auto=format&n=oS2MTbJuY6MeBjjs&q=85&s=08d8fc2cfee102ff08fd150c76b5fcf9" alt="New Feature Flag form with display name, feature ID, description, and metadata key-value entries" style={{ maxHeight: '500px', width: 'auto' }} width="1196" height="776" data-path="images/entitlements/feature-flags/create.png" />
    </Frame>
  </Step>

  <Step title="Add Metadata (Optional)">
    Turn on **Meta Data** to attach key-value configuration, such as limits, tier names, or quotas, that your application receives alongside the flag. Click **Add Entry** for each pair. See [Attach limits with metadata](#attach-limits-with-metadata).
  </Step>

  <Step title="Confirm">
    Click **Confirm**. The flag appears in your entitlements list, ready to attach to products.

    <Frame caption="The created feature flag. The right pane tracks every customer grant issued from it.">
      <img src="https://mintcdn.com/dodopayments/oS2MTbJuY6MeBjjs/images/entitlements/feature-flags/list.png?fit=max&auto=format&n=oS2MTbJuY6MeBjjs&q=85&s=02eedcbbf7ece9f67d375c984c5515b9" alt="Entitlements dashboard showing the Advanced Reports feature flag with its grant activity pane" style={{ maxHeight: '500px', width: 'auto' }} width="1316" height="898" data-path="images/entitlements/feature-flags/list.png" />
    </Frame>
  </Step>
</Steps>

## Attach to a Product

Open a product, or create one, and find the **Entitlements** card. Click **+** to attach existing entitlements, select your feature flag, and click **Done**.

<Frame caption="Attaching the feature flag to a product. One product can deliver multiple entitlements.">
  <img src="https://mintcdn.com/dodopayments/oS2MTbJuY6MeBjjs/images/entitlements/feature-flags/attach-picker.png?fit=max&auto=format&n=oS2MTbJuY6MeBjjs&q=85&s=da2137873e285cc6ce0ce234fe899ed3" alt="Entitlements attach panel with the Advanced Reports feature flag selected" style={{ maxHeight: '500px', width: 'auto' }} width="1316" height="898" data-path="images/entitlements/feature-flags/attach-picker.png" />
</Frame>

The attached flag shows on the product form, and the checkout preview lists it under **Includes**.

<Frame caption="The product now includes the feature flag. Every successful purchase or active subscription grants it.">
  <img src="https://mintcdn.com/dodopayments/oS2MTbJuY6MeBjjs/images/entitlements/feature-flags/attach-to-product.png?fit=max&auto=format&n=oS2MTbJuY6MeBjjs&q=85&s=3556181389997edddde9c396d0ce408d" alt="Product form with the Advanced Reports feature flag attached in the Entitlements card" style={{ maxHeight: '500px', width: 'auto' }} width="1316" height="898" data-path="images/entitlements/feature-flags/attach-to-product.png" />
</Frame>

## Required Configuration

| Field | Required | Description |
| - | - | - |
| `feature_id` | Yes | The identifier your application checks, for example `advanced_reports`. Not unique across entitlements. |
| `feature_type` | Yes | The type of capability conferred. `boolean` is the only supported value. |

### Create via API

<CodeGroup>
  ```typescript TypeScript expandable theme={null}
  import DodoPayments from 'dodopayments';

  const client = new DodoPayments({
    bearerToken: process.env['DODO_PAYMENTS_API_KEY'],
    environment: 'test_mode',
  });

  const entitlement = await client.entitlements.create({
    name: 'Advanced Reports',
    integration_type: 'feature_flag',
    integration_config: {
      feature_type: 'boolean',
      feature_id: 'advanced_reports',
    },
    metadata: {
      tier: 'pro',
      monthly_report_limit: 100,
    },
  });
  ```

  ```python Python expandable theme={null}
  import os
  from dodopayments import DodoPayments

  client = DodoPayments(
      bearer_token=os.environ.get("DODO_PAYMENTS_API_KEY"),
      environment="test_mode",
  )

  entitlement = client.entitlements.create(
      name="Advanced Reports",
      integration_type="feature_flag",
      integration_config={
          "feature_type": "boolean",
          "feature_id": "advanced_reports",
      },
      metadata={
          "tier": "pro",
          "monthly_report_limit": 100,
      },
  )
  ```

  ```go Go expandable theme={null}
  // client is a *dodopayments.Client, for example from
  // dodopayments.NewClient(option.WithEnvironmentTestMode()); ctx is a context.Context.
  client.Entitlements.New(ctx, dodopayments.EntitlementNewParams{
    Name:            dodopayments.F("Advanced Reports"),
    IntegrationType: dodopayments.F(dodopayments.EntitlementIntegrationTypeFeatureFlag),
    IntegrationConfig: dodopayments.F[dodopayments.IntegrationConfigUnionParam](
      dodopayments.IntegrationConfigFeatureFlagConfigParam{
        FeatureType: dodopayments.F(dodopayments.FeatureTypeBoolean),
        FeatureID:   dodopayments.F("advanced_reports"),
      },
    ),
  })
  ```
</CodeGroup>

***

## Attach Limits with Metadata

A boolean flag answers "Does this customer have the feature?". Metadata answers "With what configuration?". Entitlement metadata accepts string, integer, number, and boolean values. Every grant takes a **frozen snapshot** of the entitlement's metadata when the grant is created.

The snapshot is what makes metadata safe to use for plan limits:

* Editing the entitlement's metadata later affects only **future** grants. Customers keep the limits they purchased under.
* Each grant returns its snapshot in its `metadata` field, so one API call gives you both the flag and its configuration.

For example, an `advanced_reports` flag with `{ "tier": "pro", "monthly_report_limit": 100 }` lets your application unlock the dashboard *and* enforce the 100-report quota without a second lookup. If you later raise the limit to 250, existing customers stay at 100 until they receive a new grant, for example after a plan change.

<Tip>
  Use metadata for limits and configuration, and use `feature_id` only for identity. Encoding a limit in the ID (`advanced_reports_100`) forces a new flag for every limit change and breaks your application's checks.
</Tip>

***

## Check a Customer's Features

To build the set of features a customer has, list their delivered feature flag grants. The endpoint returns one row per grant across all entitlements, and you can filter it by `integration_type` and `status`. These examples use the `client` from [Create via API](#create-via-api).

<CodeGroup>
  ```typescript TypeScript expandable theme={null}
  const features = new Map<string, Record<string, unknown>>();

  for await (const grant of client.customers.listEntitlementGrants('cus_abc123', {
    integration_type: 'feature_flag',
    status: 'Delivered',
  })) {
    if (grant.feature) {
      features.set(grant.feature.feature_id, grant.metadata ?? {});
    }
  }

  if (features.has('advanced_reports')) {
    const limit = features.get('advanced_reports')?.monthly_report_limit;
    // unlock the dashboard, enforce the limit
  }
  ```

  ```python Python expandable theme={null}
  page = client.customers.list_entitlement_grants(
      customer_id="cus_abc123",
      integration_type="feature_flag",
      status="Delivered",
  )

  # Iterating the page fetches every page of results.
  features = {
      grant.feature.feature_id: grant.metadata
      for grant in page
      if grant.feature
  }

  if "advanced_reports" in features:
      limit = features["advanced_reports"].get("monthly_report_limit")
  ```

  ```go Go expandable theme={null}
  iter := client.Customers.ListEntitlementGrantsAutoPaging(
    ctx, "cus_abc123",
    dodopayments.CustomerListEntitlementGrantsParams{
      IntegrationType: dodopayments.F(dodopayments.CustomerListEntitlementGrantsParamsIntegrationTypeFeatureFlag),
      Status:          dodopayments.F(dodopayments.CustomerListEntitlementGrantsParamsStatusDelivered),
    },
  )

  features := map[string]bool{}
  for iter.Next() {
    grant := iter.Current()
    if grant.Feature.FeatureID != "" {
      features[grant.Feature.FeatureID] = true
    }
  }
  if err := iter.Err(); err != nil {
    panic(err.Error())
  }
  ```
</CodeGroup>

<Note>
  The `feature` payload is populated only on `feature_flag` grants. It's `null` for every other integration type. See the [List Customer Grants](/api-reference/entitlements/list-customer-grants) API reference for the full response shape.
</Note>

Calling the API on every request adds latency to your hot path. Cache each customer's feature set with a short TTL (minutes, not hours), and invalidate the cache from your webhook handler when a grant changes state. Together, these keep checks fast and make revocations take effect on the next request.

***

## Lifecycle

Feature flag grants follow the standard [grant lifecycle](/features/entitlements/introduction#how-grants-work) with one simplification: there's no delivery step, so grants never sit in `Pending` and never move to `Failed`.

| Trigger | Effect |
| - | - |
| One-time payment succeeds / subscription becomes active | Grant created with `status: Delivered` and `delivered_at` set. |
| Subscription put on hold, paused, cancelled, or expired | Grant revoked with the matching `revocation_reason`. |
| Refund on a one-time payment | Grant revoked with `revocation_reason: refund`. |
| Subscription recovers (for example, dunning succeeds) or is unpaused | The revoked grant is restored to `Delivered`, with the same grant `id` and the revocation fields cleared. |
| Manual API revoke | Grant revoked with `revocation_reason: manual`. Not auto-restored on renewal. |

Grants are idempotent per entitlement and customer. While a customer has a non-revoked grant for a flag, repeat purchases and renewals don't create duplicates.

***

## Webhooks

To mirror flags into your own database instead of polling, subscribe to the [`entitlement_grant.*` events](/developer-resources/webhooks/intents/entitlement-grant):

* `entitlement_grant.created` arrives already `Delivered`, with the `feature` payload. Enable the feature.
* `entitlement_grant.delivered` fires when a previously revoked grant is restored. Enable the feature again.
* `entitlement_grant.revoked` means access was withdrawn. Disable the feature, and check `revocation_reason` to choose your messaging.

This Express handler verifies the webhook signature with the SDK, then stores the flag state:

```typescript TypeScript expandable theme={null}
import express from 'express';
import DodoPayments from 'dodopayments';

// Reads DODO_PAYMENTS_API_KEY and DODO_PAYMENTS_WEBHOOK_KEY from the environment.
const client = new DodoPayments({ environment: 'test_mode' });
const app = express();

app.post('/webhooks/dodo', express.raw({ type: 'application/json' }), async (req, res) => {
  let event: DodoPayments.UnwrapWebhookEvent;
  try {
    event = client.webhooks.unwrap(req.body.toString(), {
      headers: req.headers as Record<string, string>,
    });
  } catch {
    res.sendStatus(400); // invalid signature
    return;
  }

  const grant = 'feature' in event.data ? event.data : null;

  if (event.type.startsWith('entitlement_grant.') && grant?.feature) {
    const enabled = event.type !== 'entitlement_grant.revoked';

    // db is your application's database client.
    await db.customerFeatures.upsert({
      customerId: grant.customer_id,
      featureId: grant.feature.feature_id,
      enabled,
      config: grant.metadata ?? {},
    });
  }

  res.sendStatus(200);
});
```

Feature flags never fire `entitlement_grant.failed`, because delivery happens entirely inside Dodo Payments.

***

## Example: Pro Plan Unlocks Advanced Reports

1. **Create the flag.** Set `feature_id: advanced_reports` with metadata `{ "tier": "pro", "monthly_report_limit": 100 }`.
2. **Attach it** to your Pro Plan subscription product.
3. **A customer subscribes.** Dodo Payments creates a `Delivered` grant and fires `entitlement_grant.created`. Your webhook handler enables `advanced_reports` for the customer with a limit of 100.
4. **Your app gates the feature.** On dashboard load, check the cached feature set (or call `listEntitlementGrants`) and render the reports tab only when `advanced_reports` is present.
5. **The customer cancels.** Dodo Payments revokes the grant and fires `entitlement_grant.revoked`, and your handler disables the feature. If a subscription later recovers through dunning, `entitlement_grant.delivered` restores the feature with no code changes.

***

## Best Practices

* **Use stable feature IDs in `snake_case`.** Your application code checks these strings, so renaming one is a breaking change on both sides.
* **Use one flag per capability.** Prefer `advanced_reports` and `api_access` as two entitlements over a single `pro_bundle`, so revocation and plan combinations stay clean.
* **Drive state from webhooks, and verify with the API.** Webhooks keep your database current. The list endpoint is the source of truth for reconciliation jobs and cache misses.
* **Treat `Revoked` as immediate.** A revoked flag means the customer no longer pays for the feature. Gate on the next request, not the next session.
* **Put limits in metadata, not in code.** Changing a quota then requires only an edit to the entitlement. New customers get the new value, and existing grants keep their purchased snapshot.


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