Webhooks
Signed deliveries, outbound only
Point an endpoint at the events you care about. Two kinds are offered: what happened in the business, and what agents were allowed, refused or asked a person about — the second being the half a security review comes for.
- Signature
- X-Xyranik-Signature
- Algorithm
- HMAC-SHA256
- Tolerance
- 300s
- Body fields
- id, event, occurredAt, data
What happened in the business
- contact.createdA new contact joined a list
- contact.unsubscribedSomeone unsubscribed
- campaign.sentAn email campaign finished sending
- post.publishedA social post went live
- post.failedA platform rejected a post
- order.createdAn order was recorded
- order.attributedAn order was traced back to a campaign
- form.submittedSomeone submitted a hosted form
What an agent was allowed to do
The trust layer emits too. These are how an external system watches what software was permitted to do in the workspace — including the refusals, which is the half that usually goes unrecorded.
- agent.request.authorizedAn agent was allowed to do something
- agent.request.deniedAn agent was refused
- agent.request.approval_requiredAn agent’s request is waiting for a person
- agent.request.approvedA person approved an agent’s request
- agent.request.rejectedA person turned down an agent’s request
- agent.request.expiredNobody answered an agent’s request in time
- agent.request.completedAn authorised action was carried out
- agent.pausedAn agent was paused, suspended or revoked
- agents.stoppedEvery agent in the workspace was stopped
- agents.resumedAgents were started again
What a delivery looks like
- X-Xyranik-EventThe event type, so you can route without parsing the body.
- X-Xyranik-DeliveryA unique id for this delivery attempt, for your own de-duplication.
- X-Xyranik-SignatureThe signature, and the timestamp it was made with.
{
"id": "evt_…",
"event": "order.created",
"occurredAt": "2026-09-18T09:15:00.000Z",
"data": { … }
}Check the signature before you trust it
The header is t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>", keyed with the endpoint’s secret>. Compare against the raw body — re-serialising parsed JSON changes the bytes and the signature will not match — and compare in constant time.
const [t, v1] = header.split(',').map(part => part.split('=')[1])
const expected = createHmac('sha256', secret)
.update(t + '.' + rawBody)
.digest('hex')
const ok =
timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) &&
Math.abs(Date.now() / 1000 - Number(t)) < 300The timestamp check is what stops a captured delivery being replayed at you later.
The signing secret is sealed at rest, and rotation signs with both the old and the new secret for an overlap — so you can move to the new one without dropping a delivery.
What Xyranik will not do
- No private addressesA webhook is never delivered to a private, loopback or internal address. The address is checked when you save it and again before every send, because a hostname that resolved publicly once can resolve inward later.
- Outbound onlyThese are deliveries from Xyranik to you. Nothing about this endpoint lets anything reach inward.
- No unannounced eventsInternal events describing how Xyranik decided are deliberately not subscribable yet: their shapes are still moving, and an external contract is a promise not to move them.
The reasoning behind all three is on the Webhooks platform page.