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.

POST /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"
}
POST /auth/login
No auth

Exchange credentials for a JWT.

GET /auth/me
JWT (dashboard)

Return the current user from the JWT.

Products

A product is the app or service you license.

GET /products
JWT (dashboard)
POST /products
JWT (dashboard)

Create a product. The response includes a secret key (sk_…) which is shown once only. store it server-side.

GET /products/:id
JWT (dashboard)
PATCH /products/:id
JWT (dashboard)
DELETE /products/:id
JWT (dashboard)
POST /products/:id/api-keys
JWT (dashboard)

Generate a new public/secret key pair. Use this to rotate.

DELETE /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.

GET /licenses?productId=…
JWT (dashboard)
POST /licenses
JWT (dashboard)

Issue a new license key. The key is unique and includes a checksum.

GET /licenses/:id
JWT (dashboard)

Returns the license plus up to 100 most-recent activations.

POST /licenses/:id/revoke
JWT (dashboard)
POST /licenses/:id/reinstate
JWT (dashboard)
DELETE /licenses/:id/activations/:activationId
JWT (dashboard)

Public API /v1/*

Authenticate with the public key (pk_…). These endpoints are what your shipped client app calls.

POST /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
  }
}
POST /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
}
POST /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.created
  • license.revoked
  • license.reinstated
  • license.expired
  • activation.created
  • activation.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

GET /products/:id/webhooks
JWT (dashboard)
POST /products/:id/webhooks
JWT (dashboard)

Body: { url: string, events: string[] }. URL must be HTTPS. Returns the webhook plus the secret, shown once.

PATCH /products/:id/webhooks/:webhookId
JWT (dashboard)

Pause/resume or change url/events. Resuming clears the failure counter.

DELETE /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_error400Body failed schema validation.
unauthorized401Missing/invalid token or API key.
not_found404Resource missing or not yours.
activation_limit_reached403License at maxActivations.
conflict409Email already registered, etc.
body_too_large413Request body exceeded 64 KB.
rate_limit_exceeded429Per-IP global limit hit.
auth_rate_limited429Too many /auth/* hits from one IP.
license_rate_limited429Too 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).