Documentation
API Reference
REST over JSON. UTF-8 everywhere. Times are ISO 8601.
Base URL
https://api.paperkey.dev
The dashboard authenticates with a JWT (returned by POST /auth/login). The public API
authenticates with your public key (pk_…).
Send it as Authorization: Bearer pk_… or x-api-key: pk_….
Auth
Sign up, sign in, and inspect the current user.
/auth/register No auth Create a new dashboard account. Returns the user and a JWT (7 d).
{
"email": "you@example.com",
"password": "min-8-chars",
"name": "optional"
}/auth/login No auth Exchange credentials for a JWT.
/auth/me JWT (dashboard) Return the current user from the JWT.
Products
A product is the app or service you license.
/products JWT (dashboard) /products JWT (dashboard)
Create a product. The response includes a secret key
(sk_…) which is shown once only.
store it server-side.
/products/:id JWT (dashboard) /products/:id JWT (dashboard) /products/:id JWT (dashboard) /products/:id/api-keys JWT (dashboard) Generate a new public/secret key pair. Use this to rotate.
/products/:id/api-keys/:keyId JWT (dashboard) Revoke a key pair. Apps using it stop working immediately.
Licenses
Per-customer credentials issued against a product.
/licenses?productId=… JWT (dashboard) /licenses JWT (dashboard) Issue a new license key. The key is unique and includes a checksum.
/licenses/:id JWT (dashboard) Returns the license plus up to 100 most-recent activations.
/licenses/:id/revoke JWT (dashboard) /licenses/:id/reinstate JWT (dashboard) /licenses/:id/activations/:activationId JWT (dashboard) Public API /v1/*
Authenticate with the public key (pk_…). These
endpoints are what your shipped client app calls.
/v1/licenses/validate Public API key Check whether a license key is currently usable.
Request
{
"license_key": "K7WX9-M3NP4-H8TRC-R2",
"fingerprint": "paperkey_fp_v1_..."
}Response
{
"valid": true,
"license": {
"id": "clx...",
"status": "active",
"expiresAt": "2026-12-31T00:00:00.000Z",
"activationsCount": 1,
"activationsLimit": 3,
"isActivated": true,
"metadata": null
}
}/v1/licenses/activate Public API key
Bind a fingerprint to a license. Idempotent: re-activating the same fingerprint
refreshes lastCheckAt and does not consume a slot.
Request
{
"license_key": "K7WX9-M3NP4-H8TRC-R2",
"fingerprint": "paperkey_fp_v1_...",
"name": "MacBook Pro"
}Response
{
"success": true,
"activation": { "id": "clx...", "fingerprint": "paperkey_fp_v1_...", "name": "MacBook Pro", "createdAt": "..." },
"activationsRemaining": 2
}/v1/licenses/deactivate Public API key Release a fingerprint slot so the customer can move to another machine.
Webhooks
Subscribe to events on your product. Each delivery is a POST to your URL with an HMAC-SHA-256 signature you verify against the secret returned at create-time.
Events
license.createdlicense.revokedlicense.reinstatedlicense.expiredactivation.createdactivation.removed
Headers on every delivery
X-Paperkey-Signature: hex-encoded HMAC-SHA-256 of the raw body using your secret.X-Paperkey-Event: the event name, e.g.license.created.X-Paperkey-Delivery: unique delivery ID, useful for idempotency.
Sample payload
{
"id": "evt_4b8c…",
"event": "license.created",
"createdAt": "2026-04-25T12:34:56.000Z",
"data": {
"id": "clx…",
"key": "K7WX9-M3NP4-H8TRC-R2",
"email": "customer@example.com",
"maxActivations": 3,
"expiresAt": "2027-04-25T00:00:00.000Z"
}
}Verifying the signature
import { verifyWebhookSignature } from '@paperkeyhq/sdk';
app.post('/paperkey-webhook', async (req, res) => {
const signature = req.headers['x-paperkey-signature'];
const raw = req.rawBody.toString('utf8');
if (!verifyWebhookSignature(raw, signature, process.env.PAPERKEY_WEBHOOK_SECRET)) {
return res.status(401).end();
}
const event = JSON.parse(raw);
// ...
res.status(200).end();
});Delivery semantics
- Fire-and-forget. Failures don't block the API call that triggered the event.
- 5-second timeout per delivery.
- 10 consecutive failures auto-pauses the webhook. Resume from the dashboard.
- Each delivery is logged (status, duration, error). last 100 visible per webhook.
Endpoints
/products/:id/webhooks JWT (dashboard) /products/:id/webhooks JWT (dashboard)
Body: { url: string, events: string[] }. URL must be HTTPS. Returns the
webhook plus the secret, shown once.
/products/:id/webhooks/:webhookId JWT (dashboard) Pause/resume or change url/events. Resuming clears the failure counter.
/products/:id/webhooks/:webhookId JWT (dashboard) Errors & rate limits
All errors follow the same shape:
{ "error": "validation_error", "message": "Invalid request data", "details": [...] }Common codes
| Code | Status | When |
|---|---|---|
| validation_error | 400 | Body failed schema validation. |
| unauthorized | 401 | Missing/invalid token or API key. |
| not_found | 404 | Resource missing or not yours. |
| activation_limit_reached | 403 | License at maxActivations. |
| conflict | 409 | Email already registered, etc. |
| body_too_large | 413 | Request body exceeded 64 KB. |
| rate_limit_exceeded | 429 | Per-IP global limit hit. |
| auth_rate_limited | 429 | Too many /auth/* hits from one IP. |
| license_rate_limited | 429 | Too many calls per license key. |
Rate limits
- Per IP: 100 / min
- Per IP on
/auth/*: 5 / 15 min - Per license on
validate: 30 / min - Per license on
activate/deactivate: 10 / hour
Responses include X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds).