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

# Quyền cấp Feature Flag

> Bật các tính năng trong ứng dụng của riêng bạn dựa trên giao dịch mua. Feature flag entitlement cung cấp một capability boolean khi thanh toán và tự động thu hồi capability đó khi hủy.

> [!NOTE]
>
> Feature flag entitlement biến Dodo Payments thành một kho feature flag nhận biết trạng thái billing. Gắn một flag như `advanced_reports` vào một product, và mọi khách hàng thanh toán sẽ nhận được một grant mà ứng dụng của bạn kiểm tra thông qua API hoặc đồng bộ qua webhooks. Không cần platform bên ngoài, bước OAuth hay bước delivery: bản thân grant chính là capability.

## Những gì được cung cấp

Không có gì rời khỏi Dodo Payments. Grant **chính là** deliverable:

* Khi purchase, Dodo Payments tạo grant trực tiếp ở `Delivered`. Grant không bao giờ chuyển sang `Pending`, không cần khách hàng thực hiện hành động nào và không có bước delivery nào có thể thất bại.
* Grant chứa payload `feature` có kiểu: `{ "feature_type": "boolean", "feature_id": "advanced_reports" }`. Ứng dụng của bạn đọc `feature_id` để quyết định tính năng nào cần mở khóa.
* Khi hủy, refund hoặc revoke thủ công, grant được chuyển sang `Revoked` và flag biến mất khỏi các grant đã cung cấp cho khách hàng.

Các trường hợp sử dụng phổ biến gồm kiểm soát tính năng theo plan (Pro mở khóa analytics), capability bổ sung (nâng cấp "API access") và các chương trình truy cập sớm được bán dưới dạng giao dịch mua một lần.

> [!NOTE]
>
> `feature_id` là một identifier do bạn chọn và không unique giữa các entitlement. Hai entitlement có thể cấp cùng một `feature_id`, chẳng hạn plan Pro theo tháng và theo năm đều cấp `advanced_reports`.

## Tạo Feature Flag

**1. Open Entitlements**

Trong dashboard Dodo Payments, vào **Entitlements** và nhấp **+** để bắt đầu tạo entitlement mới, sau đó chọn **Feature Flags**.

**2. Name the Flag**

Nhập **Display Name** cho dashboard và reports của bạn, cùng **Description** để team biết flag kiểm soát điều gì. **Feature ID** là giá trị mà ứng dụng kiểm tra. Dashboard điền giá trị này từ display name (ví dụ, "API access" trở thành `api_access`), và bạn có thể chỉnh sửa. Giá trị này không được chứa khoảng trắng.

![Biểu mẫu New Feature Flag với display name, feature ID, description và các mục key-value của metadata](https://mintcdn.com/dodopayments/oS2MTbJuY6MeBjjs/images/entitlements/feature-flags/create.png?fit=max\&auto=format\&n=oS2MTbJuY6MeBjjs\&q=85\&s=08d8fc2cfee102ff08fd150c76b5fcf9)

**3. Add Metadata (Optional)**

Bật **Meta Data** để đính kèm cấu hình key-value, chẳng hạn như giới hạn, tên tier hoặc quota, mà ứng dụng của bạn nhận được cùng với flag. Nhấp **Add Entry** cho từng cặp. Xem [Đính kèm giới hạn bằng metadata](#attach-limits-with-metadata).

**4. Confirm**

Nhấp vào **Confirm**. Flag sẽ xuất hiện trong danh sách entitlements, sẵn sàng để đính kèm vào các sản phẩm.

![Dashboard Entitlements hiển thị feature flag Advanced Reports cùng khung hoạt động grant của flag](https://mintcdn.com/dodopayments/oS2MTbJuY6MeBjjs/images/entitlements/feature-flags/list.png?fit=max\&auto=format\&n=oS2MTbJuY6MeBjjs\&q=85\&s=02eedcbbf7ece9f67d375c984c5515b9)

## Đính kèm vào Product

Mở một product hoặc tạo product mới, sau đó tìm card **Entitlements**. Nhấp **+** để đính kèm các entitlement hiện có, chọn feature flag của bạn và nhấp **Done**.

![Bảng đính kèm Entitlements với feature flag Advanced Reports được chọn](https://mintcdn.com/dodopayments/oS2MTbJuY6MeBjjs/images/entitlements/feature-flags/attach-picker.png?fit=max\&auto=format\&n=oS2MTbJuY6MeBjjs\&q=85\&s=da2137873e285cc6ce0ce234fe899ed3)

Flag đã đính kèm sẽ hiển thị trên biểu mẫu sản phẩm, và bản xem trước checkout sẽ liệt kê flag bên dưới **Includes**.

![Biểu mẫu sản phẩm với feature flag Advanced Reports được đính kèm trong thẻ Entitlements](https://mintcdn.com/dodopayments/oS2MTbJuY6MeBjjs/images/entitlements/feature-flags/attach-to-product.png?fit=max\&auto=format\&n=oS2MTbJuY6MeBjjs\&q=85\&s=3556181389997edddde9c396d0ce408d)

## Cấu hình bắt buộc

| Field | Required | Description |
| - | - | - |
| `feature_id` | Yes | Identifier mà ứng dụng của bạn kiểm tra, ví dụ `advanced_reports`. Không unique giữa các entitlement. |
| `feature_type` | Yes | Loại capability được cấp. `boolean` là giá trị duy nhất được hỗ trợ. |

### Tạo qua API

```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"),
    },
  ),
})
```

***

## Đính kèm giới hạn bằng Metadata

Một flag boolean trả lời câu hỏi "Khách hàng này có tính năng không?". Metadata trả lời câu hỏi "Với cấu hình nào?". Entitlement metadata chấp nhận các giá trị kiểu string, integer, number và boolean. Mỗi grant nhận một **snapshot cố định** của metadata entitlement tại thời điểm grant được tạo.

Snapshot giúp metadata an toàn khi sử dụng cho các giới hạn của plan:

* Việc chỉnh sửa metadata của entitlement sau đó chỉ ảnh hưởng đến các grant **trong tương lai**. Khách hàng vẫn giữ các giới hạn theo plan đã mua.
* Mỗi grant trả về snapshot trong field `metadata`, vì vậy một API call cung cấp cả flag và cấu hình của flag.

Ví dụ, một flag `advanced_reports` với `{ "tier": "pro", "monthly_report_limit": 100 }` cho phép ứng dụng của bạn mở khóa dashboard *và* thực thi quota 100 report mà không cần lookup lần hai. Nếu sau đó bạn tăng giới hạn lên 250, khách hàng hiện tại vẫn giữ giới hạn 100 cho đến khi nhận grant mới, chẳng hạn sau khi đổi plan.

> [!TIP]
>
> Sử dụng metadata cho các giới hạn và cấu hình, còn chỉ sử dụng `feature_id` cho identity. Việc mã hóa giới hạn trong ID (`advanced_reports_100`) buộc bạn phải tạo flag mới cho mỗi lần thay đổi giới hạn và làm hỏng các kiểm tra của ứng dụng.

***

## Kiểm tra các tính năng của khách hàng

Để tạo tập hợp các tính năng mà khách hàng có, hãy liệt kê các feature flag grant đã cung cấp cho họ. Endpoint trả về một row cho mỗi grant trên tất cả entitlement, và bạn có thể lọc theo `integration_type` và `status`. Các ví dụ này sử dụng `client` từ [Tạo qua API](#create-via-api).

```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())
}
```

> [!NOTE]
>
> Payload `feature` chỉ được điền trên các grant `feature_flag`. Với mọi integration type khác, giá trị là `null`. Xem tài liệu API [List Customer Grants](/api-reference/entitlements/list-customer-grants) để biết đầy đủ response shape.

Gọi API trong mỗi request sẽ làm tăng latency trên hot path. Hãy cache feature set của từng khách hàng với TTL ngắn (tính bằng phút, không phải giờ), và invalidate cache từ webhook handler khi trạng thái grant thay đổi. Kết hợp lại, các cách này giúp việc kiểm tra nhanh và khiến việc revoke có hiệu lực từ request tiếp theo.

***

## Vòng đời

Feature flag grant tuân theo [grant lifecycle](/features/entitlements/introduction#how-grants-work) tiêu chuẩn với một điểm đơn giản hóa: không có bước delivery, vì vậy grant không bao giờ nằm ở `Pending` và không bao giờ chuyển sang `Failed`.

| Trigger | Effect |
| - | - |
| Thanh toán một lần thành công / subscription trở thành active | Grant được tạo với `status: Delivered` và `delivered_at` được thiết lập. |
| Subscription bị đưa vào trạng thái hold, paused, cancelled hoặc expired | Grant bị revoke với `revocation_reason` tương ứng. |
| Refund cho một khoản thanh toán một lần | Grant bị revoke với `revocation_reason: refund`. |
| Subscription khôi phục (ví dụ, dunning thành công) hoặc được unpause | Grant đã revoke được khôi phục về `Delivered`, với cùng `id` của grant và các field revocation được xóa. |
| Revoke thủ công qua API | Grant bị revoke với `revocation_reason: manual`. Không tự động khôi phục khi renewal. |

Grant là idempotent theo từng entitlement và customer. Khi khách hàng có grant chưa bị revoke cho một flag, các lần purchase và renewal lặp lại sẽ không tạo bản sao.

***

## Webhooks

Để mirror các flag vào database của riêng bạn thay vì polling, hãy subscribe vào các [event `entitlement_grant.*`](/developer-resources/webhooks/intents/entitlement-grant):

* `entitlement_grant.created` xuất hiện khi đã ở trạng thái `Delivered`, cùng payload `feature`. Bật tính năng.
* `entitlement_grant.delivered` được phát khi một grant trước đó bị revoke được khôi phục. Bật lại tính năng.
* `entitlement_grant.revoked` cho biết quyền truy cập đã bị thu hồi. Tắt tính năng và kiểm tra `revocation_reason` để chọn nội dung messaging.

Express handler này xác minh chữ ký webhook bằng SDK, sau đó lưu trạng thái flag:

```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 flag không bao giờ phát `entitlement_grant.failed` vì delivery diễn ra hoàn toàn bên trong Dodo Payments.

***

## Ví dụ: Plan Pro mở khóa Advanced Reports

1. **Tạo flag.** Thiết lập `feature_id: advanced_reports` với metadata `{ "tier": "pro", "monthly_report_limit": 100 }`.
2. **Đính kèm flag** vào product subscription Pro Plan.
3. **Khách hàng đăng ký.** Dodo Payments tạo grant `Delivered` và phát `entitlement_grant.created`. Webhook handler của bạn bật `advanced_reports` cho khách hàng với giới hạn 100.
4. **Ứng dụng kiểm soát tính năng.** Khi dashboard tải, kiểm tra feature set đã cache (hoặc gọi `listEntitlementGrants`) và chỉ render tab reports khi `advanced_reports` hiện diện.
5. **Khách hàng hủy.** Dodo Payments revoke grant và phát `entitlement_grant.revoked`, còn handler của bạn tắt tính năng. Nếu subscription sau đó khôi phục thông qua dunning, `entitlement_grant.delivered` sẽ khôi phục tính năng mà không cần thay đổi code.

***

## Best Practices

* **Sử dụng feature ID ổn định trong `snake_case`.** Code của ứng dụng kiểm tra các string này, vì vậy việc đổi tên một string là breaking change ở cả hai phía.
* **Sử dụng một flag cho mỗi capability.** Ưu tiên `advanced_reports` và `api_access` dưới dạng hai entitlement thay vì một `pro_bundle` duy nhất, để việc revoke và kết hợp plan luôn rõ ràng.
* **Điều khiển state từ webhooks và xác minh bằng API.** Webhook giúp database của bạn luôn cập nhật. List endpoint là source of truth cho các job reconciliation và cache miss.
* **Xử lý `Revoked` ngay lập tức.** Flag đã revoke có nghĩa là khách hàng không còn trả tiền cho tính năng đó. Hãy kiểm soát từ request tiếp theo, không phải session tiếp theo.
* **Đặt giới hạn trong metadata, không đặt trong code.** Khi thay đổi quota, bạn chỉ cần chỉnh sửa entitlement. Khách hàng mới nhận giá trị mới, còn các grant hiện tại giữ snapshot đã mua.


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