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):
| Scope | Grants |
|---|---|
entitlements:read | validate/heartbeat on behalf of your product |
licenses:issue | issue licenses for sales made outside Skape |
licenses:revoke | revoke licenses you issued |
licenses:manage | change the customer email on a license you issued |
export:read | pull signed license and usage exports |
purchases:read | read 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 60 seconds (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)
Configure purchase model, delivery, feature keys, seats and offline policy on your product's Licenses page. A perpetual access duration and permission to remain disconnected are separate settings. Permanent offline activation requires a perpetual one-time installed product; subscriptions and SaaS cannot opt into it.
Request the signed access contract during online validation:
GET /api/v1/license/validate
Authorization: Bearer <customer license JWT>
X-License-Access: v1
The response adds productId and accessDocument, a signed document binding the platform, product, license, issue time, complete entitlements and offline mode. A JWT alone does not contain offline permissions or feature policy.
GET /.well-known/jwks.json (cache 1h; no auth)
Download the installed verifier and persistent cache adapter, keeping both files together. Install jsonwebtoken and canonicalize in your Node application. Configure expectedProductId and the exact HTTPS licensing origin. Store the cache privately per customer license; the adapter writes atomically. The kit manifest records source checksums.
- Verify RS256 only, exact product/license binding, protected headers and signatures. Reject duplicate, unknown or revoked key IDs; active, next and retired keys remain trusted.
- Cache signed policy and authenticated public keys before disconnecting. During a real outage every network endpoint may be unavailable.
- Measure periodic grace from the signed grant's issuedAt and enforce the earlier paid-through expiry. Refresh the signed grant online; an unsigned heartbeat cannot extend it.
- Explicit server denials clear cached permission. Only network outages, 429 and 5xx may use a still-valid cached grant.
- Persist the highest observed time and reject clock rollback. Files and clocks controlled by an end user are not tamper-proof enforcement.
- Permanent offline activation keeps working indefinitely; revocation and seat changes reach that installation only when it reconnects.
For initial disconnected installation, your backend can obtain an owned individual installed-product bundle. Include licenseToken, accessDocument and the trusted JWKS in the installer, then configure offlineDocument and trustedKeys in the verifier. Never include a vendor API secret.
POST /api/v1/vendor/licenses/:id/refresh
Authorization: Bearer <vendor key with entitlements:read>
Content-Type: application/json
{"offlineBundle":true}
The same endpoint accepts a licenseToken for expired-token refresh. Signature identity is checked, then current ownership, access status and paid dates are authoritative; refresh never extends a term or restores denied access. Shared organization tokens and SaaS access use the account lease flow below.
Installed account and organization enrollment
For installed products using authenticated accounts, call POST /api/v1/vendor/access from your backend with productId, subjectId, verifiedEmail on first binding, and deviceId. The response.installation contains licenseToken, accessDocument and jwks. Enrollment consumes the existing device quota and repeated enrollment of the same device is idempotent. SaaS-only plans cannot issue installation bundles.
const bundle = response.installation;
const result = await verifyLicense({
baseUrl: "https://agentskope.com",
licenseToken: bundle.licenseToken,
expectedProductId: productId,
expectedAccount: { vendorId, subjectId, deviceId },
offlineDocument: bundle.accessDocument,
trustedKeys: bundle.jwks.keys,
cache: createLicenseFileCache(privateCachePath),
});
The verifier reconnects through POST /api/v1/license/installation/validate using the signed enrollment document. Each reconnect rechecks the active device registration, license state, identity binding and current organization membership and seat. Deactivation or seat removal denies reconnect without silently enrolling again. Only an explicit authenticated backend enrollment can reactivate a device. Existing offline permission lasts according to its signed policy; permanent disconnected installations cannot receive immediate revocation. Device fingerprints and locally controlled files are not tamper-proof hardware identity.
SaaS account access
Call from your trusted backend after authenticating the customer through your identity provider. Derive subjectId from the stable IdP subject and verifiedEmail from its verified identity. Do not accept a browser's emailVerified boolean or expose your vendor key.
POST /api/v1/vendor/access
Authorization: Bearer <vendor key with entitlements:read>
Content-Type: application/json
{"productId":"<product UUID>","subjectId":"<stable IdP subject>","verifiedEmail":"customer@example.com"}
The first call binds that vendor/product/subject to eligible individual or assigned organization access. When multiple eligible licenses match, supply the customer's selected licenseId; never choose silently. Direct customers require an explicit licenseId and matching encrypted customer identity. Repeat the call with productId and subjectId to refresh. Identity changes require explicit reassignment; refresh does not silently rebind.
The signed lease expires within five minutes and is capped by the paid-through date. Every refresh rechecks ownership, current license status, active organization membership and seat assignment. Removing a seat denies the next refresh; previously issued leases may last until their short expiry. Never substitute a shared organization license JWT for this account check.
Download accountVerifier.ts alongside verifier.ts. Verify purpose, platform, vendor, product and the current authenticated subject before enabling protected server features. Enforce feature keys and seats in your application; do not trust client-side UI checks. A lease cannot be reused by another subject or used beyond its expiry. With verified activation on, the answer is a v2 access document instead: verify it with verifyVendorAccountAccessV2 from the same file, which needs activationClient.ts next to it (also in the kit). The v1 verifyVendorAccountLease rejects the v2 document, and key-only validation routes answer 410.
Direct subscription lifecycle
Your backend owns direct-channel billing. After validating your billing provider's webhook signature, send the authoritative access event:
PATCH /api/v1/vendor/licenses/:id
Authorization: Bearer <vendor key with licenses:issue>
Content-Type: application/json
{"eventId":"payment-event-123","version":2,"action":"renew","paidUntil":"2099-01-01T00:00:00Z","seats":3}
Use a monotonically increasing version for each license, persisted in your backend. Identical retries return replay; older versions do not undo newer access; conflicting payloads at the current version return 409. Actions are renew, suspend and restore. Renewal can select an owned active templateId for a plan change. Restore cannot recover an unpaid expired term. Revoked access cannot be restored. Marketplace licenses cannot be changed through this direct-channel API; its changes do not write marketplace invoices, ledger or payouts. Reconcile your provider's current paid-through state if delivery was missed.
Acceptance before launch
The Licenses page reports configuration issues and a configuration fingerprint. Configuration completion does not mean integration acceptance passed. Run purchase/activation, product/feature/seat restrictions, renewal, expiry, payment failure/recovery, revocation and disconnected or removed-seat tests for that exact configuration. Re-run acceptance after changing policy or plans. Download the reference checker with both sibling verifiers and set expectedProductId. Its offline test disconnects every endpoint after caching a signed online grant.
5. Error codes
| HTTP | Meaning |
|---|---|
| 200 | valid — read entitlements.status |
| 403 | denied (uniform body; distinguish via a prior 200 payload) |
| 429 | rate limited — honor Retry-After |
| 5xx | 1Scribe 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://agentskope.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:
| Capability | 1Scribe-issued | Self-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:
| HTTP | Meaning |
|---|---|
| 400 | missing Idempotency-Key, or validation error (bad email) |
| 404 | product not found / not yours (issue), or license not yours (revoke) |
| 409 | Idempotency-Key reused with a different request body |
| 422 | bad templateId / seats out of the product's configured range |
| 429 | daily 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.plan_changed | license.user_activated | license.seat_freed | license.user_revoked | license.identity_changed
license.expiring fires once for each license, 14 days before validUntil, even though the scan runs daily. On platforms with lifecycle reminders switched on it fires at 14, 7, 3, 1 and 0 days before validUntil (UTC dates), and the payload carries daysRemaining.
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:
purchaseType | Meaning |
|---|---|
subscription | 1Scribe subscription |
one_time_perpetual | one-time purchase, no expiry |
one_time_pass | one-time purchase with a fixed access period |
legacy_stripe_subscription | older Stripe-billed recurring order |
Each row also carries purchaseId, productId, licenseId, status, startedAt, updatedAt, revokeCount (how many times the license was revoked; 0 without a license), 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.
11. Plans, limits and changing a license
Plans (SKUs) and their limits are defined on your product's Licenses page. Read them, issue by plan, move a license to another plan, change the customer on a license, and revoke with a reason, all from your server.
GET /api/v1/vendor/products/:id/plans
Authorization: Bearer <vendor key, scope entitlements:read>
Returns each plan's sku, name, rank, seat options, devices per user, features and limits. Issue by plan with planSku in the body of POST /api/v1/vendor/licenses (instead of templateId; naming both is a 400 plan_ambiguous).
POST /api/v1/vendor/licenses/:id/plan
Authorization: Bearer <vendor key, scope licenses:issue>
{ "planSku": "pro" }
For licenses you bill yourself. 200 { changed, planSku }; 409 if the license is not active; 422 if the plan is unknown or the license seats exceed the plan's options. The license key is replaced, the customer is emailed the new one when the platform sends your mail, and license.plan_changed is raised.
PUT /api/v1/vendor/licenses/:id/customer
Authorization: Bearer <vendor key, scope licenses:manage>
{ "email": "new.person@example.com" }
Changes the customer on a license you issued. The seat and devices stay; the previous address loses access at its next refresh and is told by email; license.identity_changed is raised (id only, read the new address from the API). A license can change customer once in any 24 hours (429 with Retry-After); 409 if the address is the same or already holds a seat. This scope is separate on purpose: grant it only to the key that needs it.
POST /api/v1/vendor/licenses/:id/revoke
Authorization: Bearer <vendor key, scope licenses:revoke>
{ "category": "breach", "reason": "Resold the key on a public forum" }
One license per request. category is breach, abuse, competitor or other; reason is 10 to 1000 characters. The person is told the category only, never your text. Above your daily limit (the larger of 10 or a twentieth of the product's active licenses) the request is held for review and answered 202. A repeat is 200 already_revoked.
The full contract, with every field, is the OpenAPI document at GET /api/v1/vendor/openapi.json.
12. Verified activation (v2)
With verified activation on, a license key alone no longer activates a device: the person enters the key and their email, receives a one-time code, and the platform returns a signed access document bound to that person and device. The key-only validate and heartbeat routes answer 410.
POST /api/v1/activation/code { key, email, device } -> 202 { challengeId, expiresAt }
POST /api/v1/activation/verify { challengeId, code, device } -> 200 { accessDocument, jwks }
POST /api/v1/activation/refresh { accessDocument, device } -> 200 { accessDocument }
The code works only from the device that asked for it, for 10 minutes, five tries. The access document carries accessEndsAt, the earliest end that applies to that person (license term, external key, machine token, re-verification plus grace): refuse the document from then on, whatever the offline setting. Download activationClient.ts from the integration kit; it requests the code, activates, refreshes and checks the stored document offline.
For a SaaS product that vouches for its signed-in user, POST /api/v1/vendor/access answers with a short lease in the same document format (user kind vouched, at most five minutes). Verify it with verifyVendorAccountAccessV2 from accountVerifier.ts in the kit.
13. Test mode
Create a key with the Test mode in Integrations. Its secret starts with scribe_test_ and works on the same endpoints, with these differences:
- Licenses it issues are flagged as test. A test key sees and changes only test licenses, and a live key never sees them.
- Test licenses do not count toward your issue limits, the daily revocation limit, analytics, or billing counts.
- Webhooks, emails and signatures behave as for live licenses, so you can run the whole flow end to end.