Every key carries its own seal.
Sealcord signs each licence key with your product’s Ed25519 key, so your app checks it with no network at all. Your checkout issues keys with one API call, whatever takes the payment. When the app can reach Sealcord, it counts machines against the seat limit.
From a payment to a working app.
Your checkout calls Sealcord
When a payment clears at your provider, your backend issues the licence with one call to the API. Pass the sale’s id as its reference, and a retried call never makes a second key. Selling on Polar or Stripe? Connect your account instead, and its webhooks issue it.
Sealcord issues a signed key
The key holds the licence id, product, machine limit and expiry, signed with your product’s key. Your backend gets it in the answer and hands it to the customer; through Polar or Stripe, the customer sees it on the page after checkout.
Your app checks it offline
The public key ships inside your app. A mistyped, tampered or foreign key fails the check on the spot, with no network call.
Each machine takes a seat
On first run the app activates the machine. Sealcord enforces the limit and returns a signed verdict the app keeps, so it keeps working on a plane.
A key is a signed document.
Nothing in it is secret, and nothing in it can be changed. Change one character and the signature no longer matches, so your app refuses it before it asks anyone.
The example key: the prefix ACN1-, then 104 characters of licence and 86 characters of signature.
- Product prefix
- Licence, 78 bytes
- Ed25519 signature, 64 bytes
| Licence id | 7f3a2c19-4b1e-4d7a-9a2f-1c6b8e0d5a43 |
|---|---|
| Product | Acme Notes, major version 1 |
| Issued | 12 August 2026, 10:04 UTC |
| Expires | Never |
| Limit | 3 machines |
| Issued to | customer-0142@example.com |
| Signed over | acme-notes/licence/v1, a zero byte, then the licence bytes |
Perpetual, or for a term.
In the French royal chancery, a grant meant to last forever hung its seal from laces of red and green silk; a grant for a time hung from a strip of parchment. Sealcord keeps the same distinction, in the key and in the Console, so you can tell a licence for life from a subscription at a glance.
- ActivePerpetual
PerpetualSilk cord. The key never expires; activations still apply. - ActiveUntil 30 Sep 2027
For a termParchment tag. A yearly key, or a subscription that ends when billing ends. - TrialTrial ends 14 Oct 2026
TrialYellow wax. A short term you issue by hand or from your checkout. - RevokedRevoked
RevokedThe cord is cut. Apps refuse it at their next online check.
Seats you can see.
A licence allows a number of machines or seats. Every activation is visible to you and to the customer, who can free a seat without writing to you.
- Activating the same machine twice is safe: it keeps its seat.
- Verdicts are signed with the product key and carry a “valid until” time, so an offline app knows how long to trust its last answer.
- Revoke a licence and every machine gets the news on its next check.
customer-0142@example.com
Acme Notes, perpetual- Studio iMacmacOS 26.1, Acme Notes 1.4.2Last seen 4 minutes ago
- Work laptopmacOS 26.0, Acme Notes 1.4.2Last seen yesterday
- One seat free
// the signed verdict the app keeps { "verdict": "active", "machine_id": "3b0e6f1c-…", "valid_until": 1791367440 }
Your payment provider takes the payment. Sealcord writes the licence.
Sealcord never takes your customers’ money. Any provider works through the API; a payment integration connects your own Polar or Stripe account so its webhooks issue and end licences. Map each product to a licence policy once, and every webhook is verified, recorded once and applied in order.
- PolarYour own Polar organization: a token with read access (plus portal access if you turn the portal on) and its webhook secret
- StripeYour own Stripe account: a restricted key with read access (plus portal access if you turn the portal on) and its webhook secret
- Any other providerYour own webhook handler issues through the API
| When your provider sends | Sealcord |
|---|---|
Polar order.paidStripe checkout.session.completed | Issues a licence for a one-off purchase or a new subscription. Renewals, which Stripe sends as invoice.paid, change nothing, because a subscription key has no expiry of its own. |
Polar subscription.activeStripe customer.subscription.updated | Issues the licence if it is missing, or reinstates one that ended with a subscription. |
Polar subscription.revokedStripe customer.subscription.deleted | Expires the licence, or revokes it if you prefer. |
Polar order.refundedStripe charge.refunded | Revokes the licence a fully refunded order created, unless the product is set to keep it. |
| A paid order your plan has no room for | Keeps it, and issues the licence once your plan allows. |
| Anything for an unmapped product | Records it as ignored, so you can see it arrived. |
Your customers sign in to see their licences, with the receipt for a paid purchase and, when you turn it on, your Polar or Stripe customer portal, where they see their orders and change or cancel a subscription.
One small API, for your app and your scripts.
Apps talk to three public endpoints, authenticated by the licence key itself. Your backend and scripts use scoped API keys for everything else, issuing included.
curl https://api.sealcord.com/v1/activations \ -H 'content-type: application/json' \ -d '{ "licence_key": "ACN1-AX86…s3fQ", "machine_id": "3b0e6f1c-…", "label": "Studio iMac" }' # 201 Created: the machine took a seat { "activation": { … }, "verdict": "active", "reason": null, "valid_until": "2026-10-07T10:04:00.000Z", "token": "eyJ2IjoxLCJwcm9kdWN0IjoiYWNtZS1ub3RlcyIs…" }
// reqwest and serde_json let answer: serde_json::Value = reqwest::Client::new() .post("https://api.sealcord.com/v1/activations") .json(&serde_json::json!({ "licence_key": key, "machine_id": machine_id, "label": "Studio iMac", })) .send().await? .json().await?; // answer["verdict"] is "active"; keep answer["token"]
const response = await fetch('https://api.sealcord.com/v1/activations', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ licence_key: key, machine_id: machineId, label: 'Studio iMac', }), }); const { verdict, valid_until, token } = await response.json();
Plain HTTP today. The Rust crate and the TypeScript client are planned.
- POST
/v1/activationsTake a seat for this machine - POST
/v1/activations/statusCurrent verdict for a key on a machine - POST
/v1/activations/deactivateFree the seat - POST
/v1/admin/licencesIssue a licence from your checkout, with a scoped API key
sealcordRust crate: offline check and activation@sealcord/nodeTypeScript: admin API clientPricing
Every plan signs keys the same way. Plans differ in how many active licences and products you keep. You pay the plan price and nothing else: no share of your sales, no charge per API call.
| Free$0 a monthYour first app, before it sells.Start free | Indie$19 a monthOne maker, a few apps.Start with Indie | Studio$49 a monthA shipped app with a team.Start with Studio | Business$99 a monthMany apps and buyers.Start with Business | |
|---|---|---|---|---|
| Active licences | 30 | 1,000 | 5,000 | 25,000 |
| Products | 1 | 3 | Unlimited | Unlimited |
| Payment integrations | 1 | Unlimited | Unlimited | Unlimited |
Control from where you already work.
The MCP server works through the Admin API, so an AI assistant can find a licence or revoke one when you ask it to, with the scopes you grant it.
Start in the docs.
- QuickstartCreate a product, pin its signing key and issue a first key by hand.
- The key formatEvery byte of a key, and how to check one in any language.
- Issue from your checkoutOne call from the backend that takes payment, safe to retry, with any provider.
- Activations and verdictsSeat limits, signed verdicts and how long an offline app should trust them.
- Connect Polar or StripeYour own Polar or Stripe account, product mappings, and what happens on refunds.
- API referenceEvery endpoint, error code and scope.