ReferenceUpdated 2026-11-05View as MarkdownEdit on GitHub

REST API reference

Base URL: your Keyvane deployment, e.g. https://keyvane.dev/v1.

The API is plain JSON over HTTPS, so any language works. The JavaScript SDK wraps every endpoint below.

Conventions

Authentication. Send an API key as a bearer token:

curl https://keyvane.dev/v1/products -H "Authorization: Bearer kv_sk_..."

Secret keys (kv_sk_…) can call every endpoint. Publishable keys (kv_pk_…) can only call the three license endpoints marked publishable.

Request bodies can be JSON (Content-Type: application/json) or form-encoded (curl -d key=value).

Errors use HTTP status codes and a consistent body:

{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_missing",
    "message": "customer is required.",
    "param": "customer"
  }
}
Status type Typical codes
400 invalid_request_error parameter_missing, parameter_invalid, invalid_json, feature_not_metered, idempotency_key_reused
401 authentication_error invalid_api_key
403 permission_error secret_key_required
404 not_found_error customer_not_found, product_not_found, plan_not_found, feature_not_found, license_not_found, route_not_found
409 conflict_error license_revoked, managed_by_stripe, key_taken
413 invalid_request_error payload_too_large (bodies are limited to 1 MB)
429 rate_limit_error rate_limited
500 api_error internal_error

Every response carries an X-Request-Id header. Include it when you report a problem.

Business outcomes are not errors. A usage limit being reached, a feature not in the plan, or an invalid license key all return 200 with allowed: false / valid: false and a code.

Pagination. List endpoints accept limit (1–100, default 20) and starting_after (the last id you received) and return:

{ "object": "list", "data": [ ... ], "has_more": true }

Idempotency. POST /v1/usage accepts an idempotency_key (body) or Idempotency-Key header. Replays return the original outcome with "replayed": true and are not counted again.

Rate limits. 1,200 requests per minute per secret key; 120 per minute per publishable key and client IP on license endpoints (both configurable when self-hosting). Responses include X-RateLimit-Remaining; 429 responses include Retry-After.

IDs and references. Objects have prefixed IDs (cust_…, lic_…, prd_…, pln_…). Wherever you pass a customer you can use your own user ID (external_id) instead; wherever you pass a product or plan you can use its key (pdf-analyzer, pro). product is optional when the project has exactly one product.


Entitlements

Get a customer's entitlements

GET /v1/entitlements?customer={customer}&product={product}

Returns what the customer may do right now. Unknown customers get the product's default plan (no customer record is created).

curl "https://keyvane.dev/v1/entitlements?customer=user_42" -H "Authorization: Bearer kv_sk_..."
{
  "object": "entitlements",
  "customer": { "id": "cust_01jc…", "external_id": "user_42", "email": "alex@example.com" },
  "product": { "id": "prd_01jc…", "key": "pdf-analyzer", "name": "AI PDF Analyzer" },
  "plan": { "id": "pln_01jc…", "key": "pro", "name": "Pro" },
  "source": "license",
  "license": { "id": "lic_01jc…", "status": "active", "access_until": null },
  "subscription": {
    "provider": "stripe",
    "id": "sub_1Q…",
    "status": "active",
    "current_period_end": "2026-12-05T10:00:00.000Z",
    "cancel_at_period_end": false,
    "canceled_at": null,
    "price_ids": ["price_1Q…"]
  },
  "features": {
    "pdf_analysis": {
      "name": "PDF analyses",
      "type": "metered",
      "enabled": true,
      "limit": 500,
      "used": 12,
      "remaining": 488,
      "reset_period": "month",
      "resets_at": "2026-12-01T00:00:00.000Z",
      "unit": "analyses"
    },
    "export": { "name": "Export to Markdown", "type": "boolean", "enabled": true }
  },
  "checked_at": "2026-11-05T10:00:00.000Z"
}

source is license, default_plan or none. For metered features, limit and remaining are null when unlimited.

Check a single feature

GET /v1/entitlements/check?customer={customer}&feature={feature}&quantity={n}

{
  "object": "entitlement_check",
  "allowed": false,
  "code": "not_included",
  "message": "Export to Markdown is not included in the Free plan.",
  "feature": { "key": "export", "name": "Export to Markdown", "type": "boolean", "enabled": false },
  "plan": { "id": "pln_…", "key": "free", "name": "Free" },
  "customer": { "id": "cust_…", "external_id": "user_42", "email": null }
}

code is ok, not_included or limit_exceeded. Unknown feature keys return 404 feature_not_found, which catches typos early.


Usage

Record usage

POST /v1/usage

Parameter Required Description
customer one of Your user ID (the customer is created if new)
license_key one of Meter against a license key instead
feature yes A metered feature key
quantity no Units to consume (default 1, max 1,000,000)
product no Product id or key
idempotency_key no Makes retries safe (also accepted as the Idempotency-Key header)

The limit check and the increment happen atomically. Denied attempts are not counted.

curl https://keyvane.dev/v1/usage -H "Authorization: Bearer kv_sk_..." \
  -d customer=user_42 -d feature=pdf_analysis -d idempotency_key=job_123
{
  "object": "usage",
  "allowed": true,
  "code": "ok",
  "message": "Usage recorded.",
  "replayed": false,
  "feature": { "key": "pdf_analysis", "name": "PDF analyses", "type": "metered", "enabled": true, "limit": 500, "used": 13, "remaining": 487, "reset_period": "month", "resets_at": "2026-12-01T00:00:00.000Z", "unit": "analyses" },
  "plan": { "id": "pln_…", "key": "pro", "name": "Pro" },
  "customer": { "id": "cust_…", "external_id": "user_42", "email": "alex@example.com" }
}

code is ok, limit_exceeded, not_included or license_invalid. Recording usage for an on/off feature returns 400 feature_not_metered.


Customers

Create or update a customer

POST /v1/customers. Upserts by external_id. Returns 201 when created, 200 when updated.

Parameter Description
external_id Your user ID (letters, numbers and _ . : @ | + -, max 200)
email, name Optional
metadata Up to 50 keys. Merged into existing metadata; set a key to null to remove it
{
  "object": "customer",
  "id": "cust_01jc…",
  "external_id": "user_42",
  "email": "alex@example.com",
  "name": "Alex",
  "stripe_customer_id": "cus_Q…",
  "metadata": {},
  "last_seen_at": "2026-11-05T09:58:00.000Z",
  "created_at": "2026-11-01T12:00:00.000Z",
  "updated_at": "2026-11-05T09:58:00.000Z"
}

Retrieve, update, list


Licenses

Create a license

POST /v1/licenses (secret key)

Parameter Description
plan Required. Plan key or id
product Product key or id (optional with one product)
customer Your user ID; created if new. Omit for a standalone key
expires_at ISO date/time, or YYYY-MM-DD (end of that day, UTC)
duration_days Alternative to expires_at
max_activations Device limit (defaults to the plan's)
metadata Returned to your app on validation
note Internal note, never returned to publishable keys
{
  "object": "license",
  "id": "lic_01jc…",
  "key": "KV-7K2QD-9XWMN-4RT8H-PZ3VB",
  "valid": true,
  "status": "active",
  "status_message": "License is active.",
  "access_until": "2027-11-05T10:00:00.000Z",
  "product": { "id": "prd_…", "key": "pdf-analyzer", "name": "AI PDF Analyzer" },
  "plan": { "id": "pln_…", "key": "pro", "name": "Pro" },
  "customer": { "id": "cust_…", "external_id": "user_42", "email": null },
  "source": "api",
  "expires_at": "2027-11-05T10:00:00.000Z",
  "max_activations": 3,
  "activations": 0,
  "subscription": null,
  "metadata": {},
  "note": "AppSumo #1234",
  "last_validated_at": null,
  "created_at": "2026-11-05T10:00:00.000Z",
  "updated_at": "2026-11-05T10:00:00.000Z"
}

status is the effective status: active, trialing, past_due, canceled, expired, inactive, suspended or revoked. valid says whether it currently grants access.

Retrieve, list, update

Suspend, reinstate, revoke

Validate a key

POST /v1/licenses/validate (publishable or secret key, CORS enabled)

Parameter Description
key Required. The license key
fingerprint If given, the device must be activated
product If given, the key must belong to this product
{
  "object": "license_validation",
  "valid": true,
  "code": "valid",
  "message": "License is active.",
  "license": { "object": "license", "key": "KV-…", "status": "active", "...": "..." },
  "activation": null,
  "entitlements": { "object": "entitlements", "plan": { "key": "pro" }, "features": { "...": "..." } }
}

code is one of valid, not_found, expired, suspended, revoked, subscription_canceled, payment_overdue, subscription_inactive, product_mismatch, not_activated. entitlements is null when the key is not valid.

Activate a device

POST /v1/licenses/activate (publishable or secret key)

key and fingerprint (1–200 printable characters) are required; name and product are optional. Idempotent per fingerprint. When the license is full, it returns valid: false with code: "activation_limit_reached". The response has the same shape as validation, with activation filled in.

Deactivate a device

POST /v1/licenses/deactivate (publishable or secret key) with key and fingerprint:

{ "object": "license_deactivation", "deactivated": true }

Products

{
  "object": "product",
  "id": "prd_…",
  "key": "pdf-analyzer",
  "name": "AI PDF Analyzer",
  "default_plan": "free",
  "cancellation_policy": "immediate",
  "grace_period_days": 7,
  "plans": [
    {
      "object": "plan",
      "key": "pro",
      "name": "Pro",
      "rank": 10,
      "price": { "amount": 1200, "currency": "usd", "interval": "month" },
      "max_activations": 3,
      "stripe_price_ids": ["price_1Q…"],
      "is_default": false,
      "entitlements": { "pdf_analysis": { "enabled": true, "limit": 500 }, "export": { "enabled": true } }
    }
  ],
  "features": [
    { "object": "feature", "key": "pdf_analysis", "name": "PDF analyses", "type": "metered", "reset_period": "month", "unit": "analyses" }
  ]
}

Products, plans and features are managed in the dashboard. A management API is on the roadmap.


Stripe webhooks

POST /v1/webhooks/stripe/{project_id} is called by Stripe, not by you. It is authenticated by the Stripe-Signature header. See Stripe integration.