Connect
Webhooks
Signed HTTPS calls to your server when something happens in a workspace.
Webhooks are an automation action. In Settings → Automations, pick a trigger, add a Send a webhook step and paste your HTTPS address. Atrix POSTs the event as JSON and signs every delivery.
The request
http
POST /your/endpoint HTTP/1.1
Content-Type: application/json
X-Atrix-Event: deal.won
X-Atrix-Signature: t=1761552000,v1=5f2b…
{
"event": "deal.won",
"payload": {
"dealId": "deal_1",
"title": "Brand refresh for Northwind",
"companyName": "Northwind",
"stage": "WON",
"fromStage": "PROPOSED",
"ownerId": "mem_ines",
"ownerName": "Ines Carvalho",
"href": "/dashboard/customers/deals/deal_1",
"_rule": {
"id": "rul_1",
"name": "Tell finance"
},
"_workspace": {
"name": "Acme"
}
}
}Verify the signature
The signature is an HMAC-SHA256 of {t}.{raw body} with the rule’s signing secret (whsec_…, shown when you create or rotate it). Compare in constant time, and reject old timestamps to stop replays.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret: string, header: string, rawBody: string) {
const t = /t=(\d+)/.exec(header)?.[1];
const v1 = /v1=([0-9a-f]+)/.exec(header)?.[1];
if (!t || !v1) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Delivery
- HTTPS on port 443 only. Private and loopback addresses are refused, and redirects are never followed.
- Answer with any 2xx within 8 seconds. Failures are retried with backoff, up to 3 attempts.
- Bodies are at most 64 KB and carry ids, names and dates, never message contents.
Events
| Event | Fires when | Payload fields |
|---|---|---|
offer.signed | an offer or agreement is signed | agreementId, memberId, memberName, kind, title |
employment.updated | somebody's employment record changes | memberId, memberName, fields |
member.joined | somebody joins the workspace | memberId, memberName, role, jobTitle, href |
member.offboarding.started | offboarding starts for somebody | memberId, memberName, lastDay |
leave.requested | somebody requests leave | leaveId, memberId, memberName, leaveType, startDate, endDate, days, href |
leave.decided | a leave request is approved, declined or cancelled | leaveId, memberId, memberName, leaveType, startDate, endDate, days, href, decision, deciderName |
policy.published | a policy is published | policyId, title, version, href |
policy.ack.overdue | somebody has not acknowledged a policy by its due date | policyId, title, memberId, memberName, dueDate, href |
profile_request.created | an admin asks somebody to complete their details | requestId, memberId, memberName, requestKind, dueOn |
profile_request.overdue | a request for somebody's details is overdue | requestId, memberId, memberName, requestKind, dueOn |
timesheet.submitted | a timesheet is submitted for approval | sheetId, memberId, memberName, weekStart, href |
timesheet.approved | a timesheet is approved | sheetId, memberId, memberName, weekStart, deciderName, href |
expense.submitted | an expense claim is submitted | claimId, memberId, memberName, title, href |
expense.approved | an expense claim is approved | claimId, memberId, memberName, title, deciderName, href |
invoice.sent | an invoice is sent | invoiceId, number, clientName, amountMinor, currency, href |
invoice.paid | an invoice is paid | invoiceId, number, clientName, amountMinor, currency, href |
invoice.overdue | an invoice goes overdue | invoiceId, number, clientName, amountMinor, currency, href |
deal.created | a deal is created | dealId, title, companyName, stage, fromStage, ownerId, ownerName, href |
deal.stage_changed | a deal moves to another stage | dealId, title, companyName, stage, fromStage, ownerId, ownerName, href |
deal.won | a deal is won | dealId, title, companyName, stage, fromStage, ownerId, ownerName, href |
deal.lost | a deal is lost | dealId, title, companyName, stage, fromStage, ownerId, ownerName, href |
issue.created | an issue is created | issueId, title, projectId, projectName, projectSlug, status, fromStatus, priority, assigneeId, assigneeName, actorName, href |
issue.status_changed | an issue changes status | issueId, title, projectId, projectName, projectSlug, status, fromStatus, priority, assigneeId, assigneeName, actorName, href |
issue.assigned | an issue is assigned to somebody | issueId, title, projectId, projectName, projectSlug, status, fromStatus, priority, assigneeId, assigneeName, actorName, href |
project.stage_changed | a project moves to another stage | projectId, projectName, projectSlug, stage, fromStage, href |
approval.requested | a client approval is requested | approvalId, projectId, projectName, projectSlug, title, href |
approval.decided | a client approves or asks for changes | approvalId, projectId, projectName, projectSlug, title, decision, clientName, href |
feed.post_created | somebody posts to the feed | postId, authorId, authorName, postKind, href |