Back to 1Scribe

1Scribe integration guide

Enforce a 1Scribe license across all your channels in about ten lines, with an offline fallback that keeps working through a 1Scribe outage.

1. Get a vendor API key

Vendor API keys authenticate your server-to-server calls (issuing licenses, reading exports). They are separate from the per-customer license tokens your product verifies.

In the dashboard, open Integrations, choose Create key, pick the scopes and copy the secret. The full key (scribe_live_…) is shown exactly once, so store it in your secret manager straight away. We keep only a hash and the prefix and last four characters for display.

Scopes (least privilege: grant only what a key needs):

ScopeGrants
entitlements:readvalidate/heartbeat on behalf of your product
licenses:issueissue licenses for sales made outside Skape
licenses:revokerevoke licenses you issued
export:readpull signed license and usage exports
purchases:readread purchases after accepting the Seller Agreement

Rotate with Rotate (the old and new keys both work for 24 hours, then the old one stops). Revoke is immediate (within 5 seconds). Both are audit-logged.

2. Validate a license

GET /api/v1/license/validate
Authorization: Bearer <customer license JWT>

200 { valid: true, entitlements: { tier, features, maxSeats, seatsUsed, status, offlineGraceHours, … } } means allow. 403 means deny (invalid, suspended, revoked or expired). The error body is timing-uniform, so do not try to tell the cases apart from the HTTP layer; read entitlements.status from a 200 instead. You may cache a 200 response for up to 15 minutes (honor the Cache-Control header).

3. Heartbeat

POST /api/v1/license/heartbeat
Authorization: Bearer <customer license JWT>

Mirrors validate's entitlement object and adds nextHeartbeatBy. Call it at least once before each nextHeartbeatBy so your offline-grace clock stays fresh. Rate limit: 120 requests a minute for each license.

4. Offline verification (survive 1Scribe outages)

When you cannot reach 1Scribe, verify the license token's signature locally against the published keys and keep working until the token's offlineGraceHours elapse:

GET /.well-known/jwks.json      (cache 1h; no auth)

The fallback order is online check, then offline signature check, then grace period. Key rules:

  • Verify with RS256 only. Never accept alg: none or an HS256 token signed with a public key. Pin the algorithm allowlist.
  • A token with no kid header verifies against the legacy key entry.
  • Measure grace from your last successful check, not from token issue time, in production.

5. Error codes

HTTPMeaning
200valid — read entitlements.status
403denied (uniform body; distinguish via a prior 200 payload)
429rate limited — honor Retry-After
5xx1Scribe unavailable — fall back to offline verification

6. Bring your own issuer (direct sales)

For licenses you sell outside 1Scribe, you can sign them with your own key and have your verifier accept them alongside 1Scribe-issued licenses, with no per-check round-trip to us.

verifyLicense({
  baseUrl: 'https://agentskape.com',
  licenseToken: token,
  additionalIssuers: [{ issuer: 'vendor-acme', publicKey: YOUR_PUBLIC_PEM, alg: 'RS256' }],
});

Generate a keypair:

openssl genrsa -out issuer_private.pem 2048
openssl rsa -in issuer_private.pem -pubout -out issuer_public.pem

Sign at checkout with your private key and set iss to your registered issuer id. Bind each issuer id to exactly one key: a token claiming a 1Scribe kid must never be accepted by your key, and the other way round (no fallback cascade).

What you give up with self-signed licenses:

Capability1Scribe-issuedSelf-signed (BYO)
Signature verification✅✅
Central revocation (≤60s)✅❌ (you manage)
Key rotation without redeploy✅❌
Server-controlled grace policy✅❌
Suspension on payment failure✅❌
Signed export / audit trail✅❌

Use BYO only when you handle billing and lifecycle yourself and just need offline signature checks.

7. Issue licenses for sales made outside Skape

Unlike BYO (section 6), this has 1Scribe issue and sign the license for you: the same RS256 token, the same validate, heartbeat and key path, central revocation, and key rotation without a redeploy. Use it when you want one verification integration to cover every channel (the marketplace and your own checkout).

POST /api/v1/vendor/licenses
Authorization: Bearer <vendor key, scope licenses:issue>
Idempotency-Key: <your webhook's own event id — REQUIRED>
Content-Type: application/json

{ "productId": "...", "customerEmail": "buyer@example.com", "seats": 5 }

201 { licenseId, licenseKey, origin: "vendor_direct", entitlements } on first issuance; the same body replays on 200 if you retry with the identical Idempotency-Key (safe for webhook redelivery).

Idempotency-Key is required, not optional. Set it to your own webhook or event id (for example your payment processor's event id), never a random value per attempt. A webhook redelivering the same event with the same key returns the same license; the same key with a different payload is a 409 (a bug in your retry logic, not a race condition to work around).

DELETE /api/v1/vendor/licenses/:id
Authorization: Bearer <vendor key, scope licenses:revoke>

204 on revoke (the refund or chargeback path); 404 if the license is not yours or does not exist (uniform, so there is no existence oracle).

GET /api/v1/vendor/licenses?limit=25&cursor=<last id>
Authorization: Bearer <vendor key, scope entitlements:read>

Cursor-paginated (limit up to 100, default 25), your own direct-issued licenses only.

Error codes specific to this endpoint:

HTTPMeaning
400missing Idempotency-Key, or validation error (bad email)
404product not found / not yours (issue), or license not yours (revoke)
409Idempotency-Key reused with a different request body
422bad templateId / seats out of the product's configured range
429daily issuance limit — honor Retry-After

Separate from marketplace billing: licenses you issue this way (origin: "vendor_direct") do not flow through Skape's billing, ledgers, invoices or payouts.

8. Webhooks

Register an HTTPS endpoint (in the dashboard, signed in; not available with a vendor API key, so a leaked key can never re-point your webhook) and get push notifications instead of polling:

license.issued | license.revoked | license.suspended | license.restored | license.expiring

license.expiring fires once for each license, 14 days before validUntil, even though the scan runs daily.

Every delivery is a POST to your endpoint with:

X-Scribe-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, `${t}.${rawBody}`)>
Content-Type: application/json

{"eventType":"license.revoked","licenseId":"...","occurredAt":"..."}

Payloads are stateless: no personal data and no entitlement snapshot, just enough to tell you to re-check. Verify the signature before trusting a delivery.

Retries happen after 1 minute, 5 minutes, 30 minutes, 2 hours and 8 hours, then the delivery is marked failed. Your endpoint is disabled (and you get an email) after 3 consecutive failed deliveries; a single flaky failure does not count against you, and the count resets on any successful delivery.

We never follow redirects, and we reject any endpoint URL that resolves to a private, localhost or link-local address, both when you register it and when we deliver.

9. Signed entitlement export (audit, recovery, reconciliation)

GET /api/v1/vendor/export/entitlements?from=<ISO>&to=<ISO>
Authorization: Bearer <vendor key, scope export:read>

Returns { snapshot: { generatedAt, vendorId, rows }, signature: { protected: {alg:"RS256", kid}, jws } }: every one of your own licenses (both origins, each row carries origin), including licenseKey, seats, activations, the organization's billing contact (one email) and event history. It never includes payment data, price paid, member rosters or any other vendor's data.

Limit: 10 requests per hour per vendor; rows are capped at 100,000 (413 past that: contact us). Responses are no-store; do not cache them yourself if you need the freshest state.

The signature is a detached JWS: the payload segment is empty (header..signature) by design, so snapshot stays readable in the response. To verify, re-canonicalize the snapshot you received per RFC 8785 and check it against the published keys (/.well-known/jwks.json), with the same rotation-aware trust as license tokens (retired keys stay published).

10. Purchases API (who bought what)

Availability: this endpoint needs an accepted Seller Agreement and is switched on per environment. It answers 404 until it is switched on for your account's environment.

GET /api/v1/vendor/purchases?productId=<uuid>&purchaseType=subscription,one_time_pass&status=active&updatedSince=<ISO>&cursor=<nextCursor>&limit=50
Authorization: Bearer <vendor key, scope purchases:read>

One list for subscriptions, one-time purchases and legacy Stripe subscriptions, newest change first. Every row says what it is:

purchaseTypeMeaning
subscription1Scribe subscription
one_time_perpetualone-time purchase, no expiry
one_time_passone-time purchase with a fixed access period
legacy_stripe_subscriptionolder Stripe-billed recurring order

Each row also carries purchaseId, productId, licenseId, status, startedAt, updatedAt, the billing details for its type, any discounts, and the buyer:

"buyer": { "type": "individual", "buyerRef": "opaque-stable-id", "name": "Dana Lee", "email": "dana@example.com" }

The buyer's account name and email are provided for support and upgrade purposes only (Seller Agreement section 6). buyerRef is stable for you and different for every other seller. Only settled orders are listed. A deleted account reads "Deleted user" with email: null. Payment data, internal ids and other sellers' data are never returned.

Requirements: a key created with the purchases:read scope (opt-in; create or rotate a key in Integrations) and an accepted Seller Agreement (403 {"error":"agreement_required"} otherwise). 60 requests per minute per key (429). Page with nextCursor (null on the last page); limit 1 to 100 (default 50); use updatedSince for incremental sync. Webhook payloads never contain buyer names or emails: poll this endpoint and match on licenseId.