Cool BeansCool Beans
Sign inStart free

Outbound webhooks

Cool Beans calls your server when licence lifecycle events happen, signed and retried.

Cool Beans can call your server when licence lifecycle events happen. Register an endpoint in the console (Webhooks, then Your endpoints), pick the event types you care about, and store the signing secret it returns. It’s shown exactly once.

The same registration over the admin API (self-host, bearer ADMIN_TOKEN):

curl -X POST "$COOLBEANS_URL/admin/webhooks/endpoints" \
  -H "Authorization: Bearer $COOLBEANS_ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/hooks/coolbeans","events":["license.issued","license.disabled"]}'

The response carries the endpoint and its signing secret. GET /admin/webhooks/event-types lists the valid event names, GET /admin/webhooks/endpoints lists what’s registered, and per endpoint there’s POST /admin/webhooks/endpoints/:id/rotate (returns the new secret, again exactly once), DELETE /admin/webhooks/endpoints/:id, and GET /admin/webhooks/endpoints/:id/deliveries for the delivery log.

Events

Type Fired when
license.issued A key is issued (checkout, manual, or CLI)
license.disabled A key is revoked; reason says why (refund, trial_expired, manual…)
license.reenabled A disabled key is restored
license.expiry_extended A subscription or trial expiry moved; previous_expires_at rides along
activation.created A device claimed a seat; instance carries its id and name
activation.deactivated A seat was freed: manual deactivate, the customer portal, or an expired floating lease (reason: "lease_expired")

Every payload carries event (type, created_at) and the license object exactly as the public API serializes it: key, status, kind, plan, product, expires_at.

Delivery contract

Verifying a delivery

Each request carries:

X-CoolBeans-Event: license.issued
X-CoolBeans-Signature: t=1723225200,v1=6f5a…

Recompute the signature over the raw body and compare in constant time:

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(secret: string, header: string, rawBody: string): boolean {
	const t = Number(header.match(/t=(\d+)/)?.[1]);
	const v1 = header.match(/v1=([0-9a-f]+)/)?.[1];
	if (!Number.isFinite(t) || !v1) return false;
	if (Math.abs(Date.now() / 1000 - t) > 300) return false; // refuse stale timestamps
	const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
	const a = Buffer.from(v1, 'hex');
	const b = Buffer.from(expected, 'hex');
	return a.length === b.length && timingSafeEqual(a, b);
}

Rotate the secret from the console at any time. The new value is shown once, and deliveries sign with it immediately.

What this is not

The webhook stream is for your systems: CRM, analytics, Slack. Your application should keep using the client SDK contract, so activate, validate, offline tokens, and it should never gate a feature on webhook arrival.