Automation webhooks and notifications
Automations (Lite and Standard) can start when your own system calls them, and tell other systems, your customers or your Discord server when something happens. This page covers the Webhook received trigger and the Send a webhook, Email the customer and Post in Discord actions.
Webhook received
Any system that can send an HTTP POST with JSON can start an automation: your shop, a payment provider, a CI job or a script.
Create an automation with the Webhook received trigger and save it.
Create its URL in the trigger's panel (or with
POST /api/v1/automations/{id}/hook). You get a URL likehttps://api.sunlicense.com/api/v1/automations/hooks/9nW…. Treat it like a password: anyone with it can start the automation. Rotate gives a new one and stops the old one at once.Under Body fields, say which parts of the body your conditions and placeholders use, one per line:
textorder.id string order.total double customer_email string paid boolTypes are
string,int,double,bool,timestamp(ISO 8601) andlist(a list of texts). Or paste an example body and SunLicense works the fields out. Each field becomesbody.plus its path, with_and-turned into camelCase:customer_emailisbody.customerEmail. Fields that aren't declared are ignored.Switch the automation on and call it:
curl -X POST "https://api.sunlicense.com/api/v1/automations/hooks/<token>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{"order": {"id": "1042", "total": 49.0}, "customer_email": "[email protected]", "paid": true}'The answer is 202 Accepted with {"queued": true, "runId": "…"}. The run then shows up in the automation's history. Conditions use the fields as declared, for example body.paid && body.order.total > 20.0, and placeholders as {{body.order.id}}. Every run also has webhook.receivedAt and webhook.signed.
| Answer | Why |
|---|---|
202 | Received. queued: false when the same Idempotency-Key was already received, or the run was skipped (plan, monthly limit); the history says which |
400 | The body isn't a JSON object |
401 | The webhook requires a signature and it was missing, wrong or older than 5 minutes |
404 | Unknown URL (or it was rotated) |
409 | The automation is switched off |
413 | The body is larger than 64 KB |
429 | More than 60 calls a minute to this URL |
Signed calls
Require a signature (POST /api/v1/automations/{id}/hook/secret with {"on": true}) to get a secret (whsec_…). Calls must then carry one of:
X-SunLicense-Signature: t=<unix seconds>,v1=<hex>: HMAC-SHA256 with the secret of<t>.<raw body>, within 5 minutes of now (the same scheme SunLicense uses for the webhooks it sends);X-Hub-Signature-256: sha256=<hex>: HMAC-SHA256 of the raw body (GitHub's format, so GitHub webhooks work as they are).
body='{"order":{"id":"1042"}}'
t=$(date +%s)
sig=$(printf '%s.%s' "$t" "$body" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST "$HOOK_URL" -H "Content-Type: application/json" -H "X-SunLicense-Signature: t=$t,v1=$sig" -d "$body"Send a webhook
Sends JSON to your system's URL with POST (or PUT, PATCH). Leave Body empty to send everything the run knows:
{
"automation": {"id": "…", "name": "Tell the shop"},
"run": "…",
"sentAt": "2026-10-25T12:00:00Z",
"payload": {"license.id": "…", "license.status": "SUSPENDED", "…": "…"}
}Or write your own JSON with placeholders. Put text placeholders inside quotes: {"license": "{{license.id}}", "email": "{{license.customerEmail}}"}. Values are escaped, so a customer's name can never change the shape of your JSON.
Every request is signed with your organization's signing secret (GET /api/v1/automations/signing-secret, rotate with POST …/signing-secret/rotate; both need automations.write): X-SunLicense-Signature: t=<unix seconds>,v1=<hex>, HMAC-SHA256 of <t>.<raw body>. X-SunLicense-Delivery is the run's id: the same on a retry, so you can skip a delivery you already handled.
// Node.js: check a SunLicense webhook
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}- Only addresses on the internet: SunLicense refuses private, local and cloud-metadata addresses, also when a name resolves to one or a redirect points at one.
- 5 seconds to answer. A
5xx,429or no answer is tried again after 10 seconds, 1 minute and 5 minutes; any other error fails the run with what your server said (the first 200 characters). - Redirects are followed only with
307and308; for others the run says which URL to use instead. - Needs the integrations.write permission to set up (it sends license data outside SunLicense).
Email the customer
Emails the license's own customer: the address SunLicense has for the license, never one from the trigger's data. It uses your branding and the Automation messages template under Settings → Customer emails (Lite and Standard can edit it), shows the key masked, and counts towards your daily email limit and the few emails a day one customer can get from you. Customers can unsubscribe from automation messages. The email log shows whether each one went out.
Post in Discord
- Channel: posts in the notification channel of each Discord server you connected, if its Automation messages notification is on. License keys in the message are masked (
AX8P-XXXX-XXXX-XXXX). Like license notifications, at most 10 a minute per server; the rest become one summary. - DM: messages the Discord account that claimed the license. The license's key, if the message contains it, is hidden behind a spoiler. At most 5 an hour per person.
Mentions such as @everyone never ping anyone. Both need the licenses.write permission to set up, like emails.
Test runs
A test run shows what each action would do ("Would POST signed JSON to example.com") without calling anything. A re-sent event (for example after a data fix) never starts an automation, so nothing is emailed, posted or sent twice.
