Sealcord

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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
What the licence says
Licence id7f3a2c19-4b1e-4d7a-9a2f-1c6b8e0d5a43
ProductAcme Notes, major version 1
Issued12 August 2026, 10:04 UTC
ExpiresNever
Limit3 machines
Issued tocustomer-0142@example.com
Signed overacme-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
Active
2 of 3 machines1 free
  • Studio iMac
    macOS 26.1, Acme Notes 1.4.2
    Last seen 4 minutes ago
  • Work laptop
    macOS 26.0, Acme Notes 1.4.2
    Last 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 sendsSealcord
Polar order.paidStripe checkout.session.completedIssues 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.updatedIssues the licence if it is missing, or reinstates one that ended with a subscription.
Polar subscription.revokedStripe customer.subscription.deletedExpires the licence, or revokes it if you prefer.
Polar order.refundedStripe charge.refundedRevokes the licence a fully refunded order created, unless the product is set to keep it.
A paid order your plan has no room forKeeps it, and issues the licence once your plan allows.
Anything for an unmapped productRecords 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
Planned
@sealcord/nodeTypeScript: admin API client
Planned

The API reference

Pricing

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 freeIndie$19 a monthOne maker, a few apps.Start with IndieStudio$49 a monthA shipped app with a team.Start with StudioBusiness$99 a monthMany apps and buyers.Start with Business
Active licences301,0005,00025,000
Products13UnlimitedUnlimited
Payment integrations1UnlimitedUnlimitedUnlimited

Compare every plan

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.

Connect the MCP server