What it does
- Trigger: a standard Stoaken webhook posts to an n8n Webhook node. The workflow runs on
release.pending_approval,release.publishedandrelease.rejected(see /docs/webhooks). - Check: a Code node verifies the Standard Webhooks signature before anything else runs.
- Act: later nodes post to a channel or create a task. The HTTP Request node can call the Stoaken REST API with an API key (see /docs/api).
Before you start
- n8n Cloud (any plan, or the trial) or a self-hosted n8n. Self-hosted must be reachable on the public internet over https: Stoaken never calls private addresses.
- Behind a reverse proxy, set
WEBHOOK_URL(for examplehttps://n8n.example.com/) so n8n shows the right address (docs). - Self-hosted only: allow the
cryptomodule in the Code node withNODE_FUNCTION_ALLOW_BUILTIN=crypto(set it on the task runners if you use them). n8n Cloud providescryptoalready. - Brand Admin in Stoaken, on the Team plan, to add the webhook and copy its signing secret.
- To call the API: a Stoaken API key from Settings > API keys. It acts as the person who made it, with their roles. Reads work on every plan; writes need a paid plan.
Setup
- Add a Webhook node: HTTP Method POST, a Path such as
stoaken, Authentication None, Respond Immediately. Under Options, turn on Raw Body. - In Stoaken, go to Settings > Webhooks, choose Add a webhook, set Send as to Stoaken event, paste the node's Test URL and pick the events. Copy the signing secret (shown once).
- In n8n, choose Listen for test event, then use Send a sample in Stoaken's webhook lab.
- Add a Code node (JavaScript, Run Once for All Items) after the Webhook node:
const crypto = require('crypto')
const SECRET = 'whsec_…' // the webhook's signing secret
const h = $input.first().json.headers
const raw = await this.helpers.getBinaryDataBuffer(0, 'data') // the exact bytes Stoaken sent
const ts = Number(h['webhook-timestamp'])
if (!(Math.abs(Date.now() / 1000 - ts) <= 300)) throw new Error('Stale or missing timestamp')
const key = Buffer.from(SECRET.slice('whsec_'.length), 'base64')
const expected = crypto.createHmac('sha256', key).update(`${h['webhook-id']}.${ts}.`).update(raw).digest()
const ok = (h['webhook-signature'] ?? '').split(' ').some((part) => {
const [version, sig] = part.split(',')
const got = Buffer.from(sig ?? '', 'base64')
return version === 'v1' && got.length === expected.length && crypto.timingSafeEqual(got, expected)
})
if (!ok) throw new Error('Not signed by Stoaken')
return [{ json: JSON.parse(raw.toString('utf8')) }]- Add a Switch node on
{{ $json.type }}with one output per event, then your nodes. - Edit the Stoaken webhook's address to the Production URL, then publish the workflow.
To call the API, add an HTTP Request node: Method GET, URL https://www.stoaken.com/api/releases/{{ $json.data.release_id }}, Authentication Generic Credential Type, Bearer Auth, with the stk_… key as the token.
Configuration
Recipes
- Release published, post to a channel: the
release.publishedoutput, then Slack Send Message (or Teams, Discord, Mattermost):Tokens v{{ $json.data.version }} are live: {{ $json.data.changes.total }} changes, breaking: {{ $json.data.is_breaking }}. - Release published, create a task: an If node on
data.is_breaking, then create an issue in Jira, GitHub, Linear or similar:Update to tokens v{{ $json.data.version }}. - Release needs review, notify reviewers: the
release.pending_approvaloutput, then message the reviewers' channel withhttps://www.stoaken.com/review/{{ $json.data.release_id }}. Reviewers approve or reject there. - Release rejected, tell the author: the
release.rejectedoutput, get the release, and send itsrejection_reasonto the person increated_by.
What the API gives you
- The event carries ids, the version and change counts, not names.
GET /api/themes/{id}andGET /api/brands/{id}return the theme's and Brand'sname. GET /api/releases/{id}returnsstatus,created_by,approved_by,rejection_reasonandchange_summary. Add/changesfor every change, mode by mode, or/timelinefor who sent, approved or rejected it and when.GET /api/releases/{id}/export?format=cssreturns the release as a file.- Writes need a JSON body and an
Idempotency-Keyheader (Send Headers), for example{{ $json.data.event_id }}-submit. The API can propose a release (POST /api/themes/{id}/releases) and send a draft for review (POST /api/releases/{id}/submit). That's all it can do with releases.
About the signature check
- Check against the raw body, not the parsed JSON. That's why Raw Body is on and the code reads the binary
dataproperty. - The Crypto node's HMAC takes its secret as text from a credential. Stoaken's key is the decoded bytes after
whsec_, so use the Code node. - The Webhook node's own Authentication options (Basic, Header, JWT) need credentials Stoaken doesn't send. Leave it on None and rely on the signature.
- During Rotate secret, deliveries carry two signatures for 24 hours. The code accepts either one, so paste the new secret any time in that window.
Try it
- Send test on the webhook sends a
ping. The Code node should pass it, and the Switch should send it nowhere. - Change one character of
SECRETand send a sample: the execution should fail with "Not signed by Stoaken". Put it back. - Publish the workflow, send a real release for review, and check Executions.
Troubleshooting
- Nothing arrives: the Test URL only listens after Listen for test event, and the Production URL only works once the workflow is published. Make sure Stoaken has the one you mean.
- Self-hosted address refused in Stoaken: it must be https and public. A
localhostor private-network address is never called. Cannot find module 'crypto': setNODE_FUNCTION_ALLOW_BUILTIN=cryptoand restart n8n (self-hosted).- No binary
data: Raw Body is off, or the property has another name. Check the Webhook node's output. - Every request fails the check: the secret is wrong or was rotated. The n8n server clock must also be within 5 minutes of real time.
- Webhook switched off in Stoaken: after 5 failed deliveries in a row Stoaken switches it off. Fix n8n, then turn the webhook back on in Settings > Webhooks.
- 401 from the API: check the Bearer Auth credential holds the whole
stk_…key. - 400
idempotency-key-required: a write without anIdempotency-Keyheader. 403 on a write: the key's owner lacks the role, or you're on the Free plan. - 429: a key can make 120 requests a minute and 60 writes an hour.
Limits
- With Respond Immediately, n8n answers 200 before the check runs, so a forged request gets 200 but stops at the Code node.
- The signing secret sits in the Code node, and anyone who can open the workflow can read it.
- Nothing in n8n can approve, reject or publish a release. People do that in Stoaken.
- Deliveries can arrive more than once.
webhook-idanddata.event_idare the same on every retry, so use them to skip repeats.