Skip to content

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.

  1. Create an automation with the Webhook received trigger and save it.

  2. Create its URL in the trigger's panel (or with POST /api/v1/automations/{id}/hook). You get a URL like https://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.

  3. Under Body fields, say which parts of the body your conditions and placeholders use, one per line:

    text
    order.id string
    order.total double
    customer_email string
    paid bool

    Types are string, int, double, bool, timestamp (ISO 8601) and list (a list of texts). Or paste an example body and SunLicense works the fields out. Each field becomes body. plus its path, with _ and - turned into camelCase: customer_email is body.customerEmail. Fields that aren't declared are ignored.

  4. Switch the automation on and call it:

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

AnswerWhy
202Received. queued: false when the same Idempotency-Key was already received, or the run was skipped (plan, monthly limit); the history says which
400The body isn't a JSON object
401The webhook requires a signature and it was missing, wrong or older than 5 minutes
404Unknown URL (or it was rotated)
409The automation is switched off
413The body is larger than 64 KB
429More 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).
bash
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:

json
{
  "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.

js
// 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, 429 or 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 307 and 308; 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.

Send feedback

Found something broken, or have an idea? It goes straight to our board.

The first line becomes the title.
Images (optional, up to 5: paste, drop or capture)
Check each image before sending, and remove any that show something private.

We also receive this page's address and your browser version.

Ctrl Enter to send