Webhooks
Subscribe an endpoint to workspace events, what we POST, and how to receive it safely today.
On this page
A webhook subscription POSTs to your endpoint when something happens in the workspace, so another system can react without polling.
Subscribing
Settings → Webhooks → New subscription: an event and an https:// endpoint. One subscription is one event — subscribe several times for several events.
The endpoint is validated when you save it, not when we first call it: it must be a well-formed public https:// URL. Private and loopback addresses are refused, and the refusal is a security control rather than a formatting rule.
Events
| Event | Fires when |
|---|---|
conversation.created | A conversation is opened |
conversation.updated | Status, priority or assignee changes |
message.created | A message is sent or received |
message.status_changed | An outbound message is delivered, read or fails |
order.created | An order is created |
order.verified | An order is verified |
order.refunded | An order is refunded |
insight.emitted | Revenue intelligence emits an insight |
What we send
POST, Content-Type: application/json:
{
"event": "message.created",
"payload": { "…": "the object the event is about" },
"timestamp": "2026-08-21T09:14:22.000Z",
"delivery_id": "…"
}With these headers:
| Header | Meaning |
|---|---|
X-Juno-Event | The event name, so you can route without parsing the body |
X-Juno-Signature | sha256=<hex> — HMAC-SHA256 over the exact body bytes |
X-Juno-Delivery-Id | Stable across retries of the same event — your deduplication key |
Delivery is at-least-once. The same delivery_id can arrive more than once, so make your handler idempotent: record the id and ignore a repeat. Manual redeliveries (the Redeliver button) carry no delivery_id, because they are not a retry of anything.
Return a 2xx quickly. Anything else is recorded as the subscription's last status and is visible on its row — which is where to look first when an integration has gone quiet.
Receiving it safely — read this before you act on a payload
The delivery is signed, but the signing key is not yet exposed anywhere you can read it: it is derived per workspace on the server, and no screen or endpoint returns it. Until that ships you cannot verify X-Juno-Signature, and an endpoint that acts on an unverified POST will act on anybody's POST.
So, today:
- Treat the body as a notification, not as data. Take the object's id from it and re-read the object through the API with an API key. That is authenticated, and it is also how you avoid acting on a payload that was already superseded.
- Keep the endpoint unguessable, and rate-limit it.
- Never let it perform anything destructive or financial on the strength of the POST alone.
This is a real limitation rather than an oversight in the documentation, and it is written here plainly because the alternative — describing signature verification you cannot actually perform — would leave you believing an endpoint is protected when it is not.