Webhooks
Where the API is you pulling, webhooks are Alma pushing — point one at your endpoint and Alma calls it when a workflow finishes.

Creating a webhook
Click New webhook and provide:
- Endpoint URL — must be
https:// - Events — which to subscribe to
| Event | Fires when |
|---|---|
workflow.completed | A workflow execution finished successfully |
workflow.failed | A workflow execution ended in a failed or partial state |
The webhook’s name is derived from the URL’s host automatically.
Get your signing secret immediately after creating a webhook — it’s shown only once, and the create dialog doesn’t show it. Open the webhook and click Rotate secret, which does reveal it. Without this step there’s no way to get a signing secret from the Console.
Payload
workflow.completed and workflow.failed deliver:
{
"workflow_id": "...",
"execution": {
"id": "...",
"status": "completed",
"trigger_type": "schedule",
"response_content": "...",
"completion_summary": "...",
"error": null,
"started_at": "...",
"completed_at": "..."
}
}Headers
Content-Type: application/json
User-Agent: Alma-Webhooks/1
X-Alma-Event: workflow.completed
X-Alma-Delivery-Id: <delivery id>
X-Alma-Timestamp: <ms epoch>
X-Alma-Signature: sha256=<hex HMAC-SHA256>Verifying the signature
The signature covers {timestamp}.{body}, not the body alone — signing the
body alone will never match. This also lets you reject replays: check that
X-Alma-Timestamp is recent before trusting the payload.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, headers, secret) {
const timestamp = headers["x-alma-timestamp"];
const signature = headers["x-alma-signature"];
if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) return false;
const expected =
"sha256=" +
createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Secrets are prefixed whsec_.
Delivery
Alma retries failed deliveries 5 times with exponential backoff. Any non-2xx response counts as a failure. Timeout is 10 seconds.
Redirects aren’t followed — a 3xx counts as a failed attempt, since following one could bounce the delivery somewhere it shouldn’t go. Return a 2xx quickly and do your work afterward.
Auto-disable
15 consecutive failures disables an endpoint automatically, with the reason shown on its detail page. Fix the endpoint, then click Enable. The counter resets on any success.
Monitoring

The detail page has Send test, Rotate secret, and Disable/Enable, plus a Recent deliveries table with event, status, code, duration, and any error text.
URL restrictions
Endpoints must be https:// and publicly addressable. Alma rejects
localhost, .local, .internal, and private or link-local IP ranges, both
at creation and on every send.
Testing locally? Use a tunnel (ngrok, Cloudflare Tunnel) and register the public URL.
Limits
A workspace can have 20 endpoints.
Coming soon. The Console only lets you select the two workflow events —
source events like source.github work if subscribed via the API, but
there’s no UI for them. Filters aren’t editable. Name and URL can’t be
changed after creation. There’s no delete, only Disable — which is awkward
at the 20-endpoint cap, since the error message suggests deleting one. Email
help@alma.team if you hit it.