Skip to main content
This guide walks through building a manual license key fulfillment system end to end. Instead of Dodo Payments auto-generating a key on payment, each purchase creates a pending grant and waits for you to supply the key value from your own system, a third-party vendor, or a finite pool of codes. By the end you will have:
  • A product with a License Key entitlement set to manual fulfillment.
  • A webhook listener that detects when a customer is waiting for a key.
  • A fulfillment call that delivers the key and notifies the customer automatically.

License Keys overview

The full license key lifecycle and the fulfillment_mode setting.

Fulfill License Key Grant API

API reference for the endpoint you call to deliver a key.

How It Works

Manual fulfillment changes only the issuance step. Activation, validation, deactivation, expiry, and revocation behave exactly like an auto-generated key once delivered.

Prerequisites

To follow this guide you’ll need:
  • A Dodo Payments merchant account.
  • Your API key (DODO_PAYMENTS_API_KEY) and webhook secret key from the dashboard. See the API key generation guide.
  • A backend endpoint that can receive webhooks.
Use https://test.dodopayments.com and test-mode credentials while building. Switch to https://live.dodopayments.com and live keys when you go to production.

Step 1 — Create a License Key Entitlement in Manual Mode

An entitlement is a reusable definition of what you deliver. Create a License Key entitlement and set its fulfillment_mode to manual.
1

Open Entitlements

Go to Entitlements in your dashboard and click + to create a new entitlement.
2

Choose License Key

Select License Key as the integration and give it a Name. The form exposes these fields:
  • Fulfillment ModeAutomatic by default. This is the setting that enables manual fulfillment; you change it in the next step.
  • License Length — how long each issued key stays valid, or No expiration.
  • Activations Limit — maximum activations per key, or Unlimited.
  • Activation Message — optional customer-facing message shown when they activate the key.
New License Key entitlement form with name, fulfillment mode, license length, activations limit, and activation message
3

Set Fulfillment Mode to Manual

Open the Fulfillment Mode dropdown and change it from Automatic to Manual. This is the setting that drives this entire guide — without it, keys are generated and emailed automatically and no pending grant is created. With Manual selected, each purchase creates a pending grant for you to fulfill. Click Create Entitlement to save.
fulfillment_mode defaults to auto. Omitting it, or leaving an existing entitlement unchanged, keeps the automatic behavior. Only entitlements explicitly set to manual create pending grants.

Step 2 — Attach the Entitlement to a Product

Open the product you want to sell, expand Advanced Settings → Entitlements & Credits, and select the License Key entitlement you set to Manual in Step 1. A single product can deliver this license key alongside other entitlements on the same purchase.
Product entitlements panel with License Key selected
Fulfillment mode is a property of the entitlement, not the product. Because you set it to Manual in Step 1, every product this entitlement is attached to creates pending license-key grants on purchase — there is nothing extra to configure here.
If you don’t have a product yet, create one first (one-time or subscription). See the One-time Payments Integration Guide for selling the product through checkout.

Step 3 — Detect Pending Grants

When a customer buys the product, Dodo Payments creates a grant in pending status with no key attached and fires an entitlement_grant.created webhook. This is your signal that a customer is waiting for a key.

Listen for the webhook

Set up a webhook endpoint (Developer → Webhooks in the dashboard) and act on pending license-key grants. The implementation follows the Standard Webhooks specification.
import { Webhook } from 'standardwebhooks';

const webhook = new Webhook(process.env.DODO_WEBHOOK_KEY!);

export async function POST(request: Request) {
  const rawBody = await request.text();
  const headers = {
    'webhook-id': request.headers.get('webhook-id') || '',
    'webhook-signature': request.headers.get('webhook-signature') || '',
    'webhook-timestamp': request.headers.get('webhook-timestamp') || '',
  };

  await webhook.verify(rawBody, headers);
  const event = JSON.parse(rawBody);

  // A customer bought a manual-mode license key and is waiting for it.
  if (
    event.type === 'entitlement_grant.created' &&
    event.data.integration_type === 'license_key' &&
    event.data.status === 'pending'
  ) {
    await queueLicenseKeyFulfillment({
      grantId: event.data.id,
      customerId: event.data.customer_id,
    });
  }

  return new Response('ok');
}
The grant payload carries integration_type: "license_key", so you can recognize a license-key grant without an extra lookup. See the Entitlement Grant webhook reference for the full payload.

Or poll the List Grants API

If you’d rather not rely on webhooks, list grants for the entitlement and filter by integration_type and status:
const pending = await client.entitlements.grants.list('ent_license_key_id', {
  integration_type: 'license_key',
  status: 'pending',
});

for (const grant of pending.items) {
  console.log(`Grant ${grant.id} for customer ${grant.customer_id} needs a key`);
}

Step 4 — Deliver the Key

Obtain the key value from your own system, then submit it with the Fulfill License Key Grant endpoint. This requires your secret API key (Editor permission); it is not one of the public license endpoints.
async function fulfill(grantId: string, key: string) {
  const res = await fetch(
    `https://test.dodopayments.com/grants/${grantId}/license-key`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${process.env.DODO_PAYMENTS_API_KEY}`,
      },
      body: JSON.stringify({
        key,
        // Optional — fall back to the entitlement config when omitted
        activations_limit: 5,
        expires_at: '2027-05-01T00:00:00Z',
      }),
    },
  );

  if (!res.ok) {
    // See the status code table below for handling
    throw new Error(`Fulfillment failed: ${res.status}`);
  }

  return res.json(); // updated grant, now status: "delivered"
}

Request fields

key
string
required
The license key string to deliver to the customer. Whitespace is trimmed; an empty or whitespace-only value is rejected.
activations_limit
integer
Per-key activation limit. Falls back to the entitlement config when omitted.
expires_at
string
Per-key expiry (ISO 8601). Falls back to the entitlement config’s duration when omitted. For subscription-issued grants, validity remains tied to the subscription regardless.
On success the grant moves to delivered, the customer is sent the key automatically (the same email they’d receive under auto fulfillment), and entitlement_grant.delivered fires. The customer receives an email with the license key, the product, activation limit, expiry, and your activation instructions:
Customer license key email showing the key, product, activation limit, expiry, and activation instructions
You do not need to email the key yourself — delivery happens automatically when the grant is fulfilled.

Step 5 — Handle Errors and Retries

The endpoint validates the grant before delivering anything. Handle these responses:
StatusMeaningWhat to do
200Key delivered, grant is now delivered.Done.
400Not a license-key grant, or the key is empty/whitespace.Fix the request; do not retry as-is.
404No grant with that ID for your business.Verify the grant_id.
409Grant not awaiting fulfillment (already delivered or already has a key), or the key value already exists.If already delivered, treat as success. If a duplicate key, supply a different key.
422Request body failed validation (e.g. activations_limit < 1).Correct the field and retry.
Fulfillment is safe to retry on transient errors (timeouts, 5xx). Each grant can be fulfilled only once, so a retry after a successful-but-unacknowledged call returns 409 rather than issuing a second key or sending a duplicate email. Use the grant id as your idempotency key.

Verify the Flow

  1. Buy the product in test mode (see the checkout guides).
  2. Confirm your webhook received entitlement_grant.created with status: "pending" and integration_type: "license_key", or that the grant appears in the List Grants response with those filters.
  3. Call the fulfill endpoint with a test key.
  4. Confirm the response shows status: "delivered" with a populated license_key, the customer receives the key email, and entitlement_grant.delivered fires.
Once delivered, the customer can activate and validate the key against the public license endpoints exactly like an auto-generated key.

Create Entitlement

Create the License Key entitlement with fulfillment_mode: manual.

List Grants

Filter by integration_type and status to find pending grants.

Fulfill License Key Grant

Deliver the key value and transition the grant to delivered.

Entitlement Grant Webhooks

The entitlement_grant.* events that signal pending and delivered grants.
Last modified on June 8, 2026