Documentation
Issuing licenses
POST /v1/issue mints a fresh license key and creates or dedupes a customer record. It's the right endpoint to call from a webhook handler (Stripe, Paddle, your own checkout) the moment a payment succeeds.
The API key used to call this endpoint must have FULL or ISSUE_ONLY scope.
Request
POST /v1/issue
Host: api.keystack.dev
Authorization: Bearer ak_live_4f2a...
X-KeyStack-Timestamp: 1731600000
X-KeyStack-Signature: 7b3d8a1f...
Content-Type: application/json
{
"planCode": "lifetime-pro",
"customer": {
"email": "alice@example.com",
"name": "Alice Smith",
"externalId": "stripe_cust_abc"
},
"duration": "lifetime",
"maxActivations": 3,
"notes": "Black Friday order",
"metadata": {
"stripeCheckoutId": "cs_test_abc",
"campaign": "launch-2026"
}
}Fields
| Field | Type | Required | Notes |
|---|---|---|---|
planCode | string | yes | Must match an existing plan in the application. |
customer.email | string | yes | Used for dedup + delivery email. Lowercased before lookup. |
customer.name | string | no | Cosmetic. |
customer.externalId | string | no | Your internal customer ID (e.g. Stripe customer). Merged non-destructively on re-issue. |
customer.metadata | object | no | Free-form JSON merged into the customer row. |
duration | "lifetime" | "<n>d" | ISO 8601 datetime | no | Overrides the plan's default duration. Use "30d" / "365d" / "2027-01-01T00:00:00Z". |
maxActivations | number | no | Overrides the plan's default activation limit. |
notes | string | no | Internal-only note shown in the dashboard. |
metadata | object | no | Free-form JSON, queryable in the dashboard. |
Response
{
"id": "ckxz1ab2c0000abcd",
"key": "KS-7F3K9-9XYLM-4N2A1-Q7C9F-WT2K",
"status": "ACTIVE",
"expiresAt": null,
"customer": {
"id": "cus_8a3f...",
"email": "alice@example.com"
}
}Error codes
| HTTP | code | Meaning |
|---|---|---|
| 400 | validation/invalid-input | One or more fields failed validation (check details). |
| 401 | api/key-invalid | API key was not recognised. |
| 401 | api/signature-invalid | HMAC signature did not match. |
| 401 | api/timestamp-replay | This signature was already used or X-KeyStack-Timestamp is too old. |
| 403 | authz/role-insufficient | API key scope can't issue licenses. |
| 403 | quota/licenses-exceeded | Your platform plan has hit its active-license cap. |
| 404 | common/not-found | planCode doesn't exist in this application. |
| 429 | common/rate-limited | Slow down — back off and retry. |