Webhooks
A webhook endpoint is a URL on your own server and the event types it receives. When one of your
licences or a machine changes, Sealcord sends the endpoint a signed POST within seconds. Use it
to update your own database, tell your customer, or start a workflow, without polling the API.
This page covers adding an endpoint, the events and the request. Next: verify the signature, then read how retries and disabled endpoints work.
Add an endpoint
Section titled “Add an endpoint”An owner or admin adds one in the Console, under Webhooks, Outgoing. Your backend can add one too,
with an admin API key that has the webhooks:write scope
(add an endpoint). An AI assistant
connected through the MCP server cannot add, change or remove an endpoint, or replace its
secret.
The answer that adds the endpoint carries its signing secret, whsec_…. It is shown once.
Keep it in your server’s secrets. If you lose it, replace it.
The URL
Section titled “The URL”The URL must:
- use
httpson port 443 and name a public host; - carry no user name, no password and no
#fragment; - be at most 2048 characters;
- not be an IP address (however written), an internal name (
localhost, a name without a dot, anything under.localhost,.internal,.localor.home.arpa) or anything undersealcord.com.
A URL that breaks a rule is refused when you save it. Sealcord checks again on every attempt: it
resolves the host, and refuses the attempt (address_refused) if any address it gets is not
public. To try a receiver on your own machine, expose it through a tunnel that gives it a public
https host name.
How many endpoints
Section titled “How many endpoints”A plan includes a number of endpoints: Free 1, Indie 3, Studio 10 and Business 25. Turned-off
endpoints count. Past the number, adding one answers 402 plan_limit_reached.
Events
Section titled “Events”An event is written in the same database transaction as the change it reports, so you hear about every change that happened and none that rolled back. Only changes to your organization’s licences are sent, and only to enabled endpoints subscribed to the event’s type.
| Type | When | data holds |
|---|---|---|
licence.issued |
A licence is issued: through the API, the Console or a payment integration | licence |
licence.revoked |
A licence becomes revoked: by you, or by a payment provider’s refund or subscription end |
licence |
licence.expired |
A licence is expired on purpose (the API, the Console or a payment provider’s event). A key’s expires_at passing changes nothing and sends nothing |
licence |
licence.reinstated |
A revoked or expired licence becomes active again | licence |
licence.anonymised |
The licence’s customer data was erased on your instruction. The licence in data is already anonymised. Erasing also revokes an active licence and frees its machines, and sends no licence.revoked or activation.deactivated for that |
licence |
licence.trial_converted |
A trial was paid for: its trial_ends_at is cleared |
licence |
activation.created |
A machine with no live activation on the licence is activated. Activating a machine that already holds a seat refreshes it and sends nothing | activation, licence |
activation.deactivated |
A machine’s seat is freed: by your app, the customer, or you in the Console or the API | activation, licence |
A change that changes nothing, such as revoking a licence that is already revoked, writes no
event. licence is the licence as the API shows it, never its key. activation is the machine as
the API lists it. Both are the state the change left, at the moment it happened: a retry hours
later still carries that snapshot, so read the licence from the API when you need it as it is now.
The request
Section titled “The request”Each delivery is a POST to your URL:
POST /sealcord HTTP/1.1Host: hooks.example.comContent-Type: application/jsonUser-Agent: Sealcord-Webhooks/1.0 (+https://sealcord.com)webhook-id: evt_6f0c0d5e2b9a4c1e8d7f3a2b1c0d9e8fwebhook-timestamp: 1791018300webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{"type":"activation.created","timestamp":"2026-10-03T09:05:00.000Z","data":{"activation":{…},"licence":{…}}}webhook-idis the event’s id:evt_and 32 hex characters. It is the same on every attempt and every endpoint, so de-duplicate on it.webhook-timestampis the attempt’s time in Unix seconds. It changes on every attempt and is part of what is signed.webhook-signatureisv1,and a base64 signature. There is one entry per signing secret, separated by spaces: two, for 24 hours after you replace the secret.- The body is one line of JSON, the same bytes on every attempt, at most 64 KB. A larger event
is never sent: its delivery fails with
payload_too_large.
An example body
Section titled “An example body”An activation.created event for Acme Notes, formatted for reading:
{ "type": "activation.created", "timestamp": "2026-10-03T09:05:00.000Z", "data": { "activation": { "id": "10000000-0000-4000-8000-000000000001", "licence_id": "00000000-0000-4000-8000-000000000001", "machine_id": "ada-mbp-2024", "label": "Ada's MacBook Pro", "os": "macos 26.1", "app_version": "2.0.3", "activated_at": "2026-10-03T09:05:00.000Z", "last_seen_at": "2026-10-03T09:05:00.000Z", "deactivated_at": null }, "licence": { "id": "00000000-0000-4000-8000-000000000001", "product_id": "acme-notes", "email": "ada@example.com", "issued_at": "2026-10-01T12:00:00.000Z", "expires_at": null, "limit_kind": "machines", "limit_count": 2, "major": 2, "status": "active", "status_reason": null, "status_changed_at": null, "trial_ends_at": null, "source": "admin", "integration_id": null, "external_ref": "order-1042", "polar": { "order_id": null, "subscription_id": null, "customer_id": null, "checkout_id": null }, "anonymised_at": null, "created_at": "2026-10-01T12:00:00.000Z", "updated_at": "2026-10-01T12:00:00.000Z" } }}timestamp in the body is when the change happened, and type is the event’s type. Fields may be
added to licence and activation as the API grows: ignore the ones you do not know.
Answer quickly and de-duplicate
Section titled “Answer quickly and de-duplicate”- Answer
2xxwithin 5 seconds. The 5 seconds cover the lookup, the connection and your status line. Store the event, answer, and do the work afterwards. Sealcord keeps only your status code: it never reads the body of your answer. - A redirect is a failure. Sealcord follows none, so give the final URL.
- The same event can arrive more than once. Delivery is at least once: a retry after a timeout
you did answer, a sender that stopped mid-send, or a retry from the Console. Record each
webhook-idyou handled and skip a repeat. Keep them for 30 days, because a delivery can be retried by hand until its event is deleted, 30 days after it happened. - Events can arrive out of order after a retry. Order by
timestampin the body, or read the licence from the API.
Questions
Section titled “Questions”Does a webhook carry the licence key?
Section titled “Does a webhook carry the licence key?”No. An event carries the licence as the API shows it, without its key. Read the licence from the Admin API when you need the key.
Does Sealcord send a webhook when a licence’s expiry date passes?
Section titled “Does Sealcord send a webhook when a licence’s expiry date passes?”No. A key’s expires_at passing changes no status, so it sends nothing. licence.expired is sent
when a licence is expired on purpose.
Can I send webhooks to a server on my laptop?
Section titled “Can I send webhooks to a server on my laptop?”Only through a tunnel that gives it a public https host name on port 443. Sealcord refuses
localhost, IP addresses and internal names.
What if my server was down?
Section titled “What if my server was down?”Sealcord retries for about 55 hours. See retries and disabled endpoints. While an endpoint is turned off, changes are not sent to it: read what changed from the API.