Appearance
Webhooks
Instead of polling the API, let Artesa call you: a webhook is an HTTP POST to your endpoint whenever data you subscribed to changes.
Subscribe
- In Artesa: Settings → Public API → Webhooks (admins only).
- Through the API:
POSTto/v2/webhook-subscriptionswith aurland theeventsyou want. The response carries the subscription'ssecret.
The url must use HTTPS and be reachable from the internet. Set deactivatedAt to pause a subscription and null to resume it.
Events
Every resource sends <resource>.created, <resource>.updated and <resource>.deleted, for example assignment.updated. data is the resource exactly as its v2 GET returns it; for *.deleted only its uuid.
| Resource | Endpoint | Also *.updated when |
|---|---|---|
assignment | /v2/assignments | its address changes |
assignment-upload | /v2/assignments/{uuid}/uploads | |
assignment-event | /v2/assignment-events | users or gangs are assigned, an address changes |
assignment-event-upload | /v2/assignment-events/{uuid}/uploads | |
assignment-event-series | /v2/assignment-event-series | |
assignment-event-group | /v2/assignment-event-groups | |
assignment-event-group-type | /v2/assignment-event-group-types | |
assignment-event-type | /v2/assignment-event-types | |
assignment-status-type | /v2/assignment-status-types | |
assignment-type | /v2/assignment-types | |
customer | /v2/customers | its billing address changes |
gang | /v2/gangs | members join or leave |
project | /v2/projects | |
role | /v2/roles | |
time-entry | /v2/time-entries | its day is submitted, approved or declined, or its computed duration changes |
time-entry-type | /v2/time-entry-types | |
time-sheet-day | /v2/time-sheet-days | its times are recalculated |
time-sheet-week | /v2/time-sheet-weeks | |
time-sheet-month | /v2/time-sheet-months | |
upload-category | /v2/upload-categories | its roles change |
user | /v2/users | |
user-absence | /v2/user-absences | its work days are counted |
user-absence-type | /v2/user-absence-types |
A series' occurrences are assignment-events and send their own events: setting up a series sends one assignment-event.created for each occurrence of the coming twelve months, and later ones follow as the series moves on daily.
Computed values arrive in a later *.updated: a new absence first has workDays: 0, a new upload appears once its file is complete.
Treat *.created and *.updated alike as "upsert this": an upload attached to an assignment later arrives as assignment-upload.created then, and a change you missed while your endpoint was down is only in the next event.
Not every change sends an event. Rows deleted together with their parent — an assignment's work steps, a deleted user's time entries — send no *.deleted of their own, and customer imports send nothing. Reconcile with the list endpoints from time to time if you mirror data.
Payload
json
{
"type": "assignment.updated",
"timestamp": "2026-10-16T08:30:00.000Z",
"data": { "uuid": "01a14371-cc14-7524-8225-672c59cef75a", "name": "…" }
}timestamp is when the change happened.
Verify the signature
Every request is signed per Standard Webhooks. Ready-made verifiers exist for many languages — pass them the subscription's secret and the raw request body. By hand:
js
import { createHmac, timingSafeEqual } from "node:crypto";
function isFromArtesa(secret, headers, rawBody) {
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const signed = `${headers["webhook-id"]}.${headers["webhook-timestamp"]}.${rawBody}`;
const expected = Buffer.from(createHmac("sha256", key).update(signed).digest("base64"));
const fresh = Math.abs(Date.now() / 1000 - Number(headers["webhook-timestamp"])) < 5 * 60;
return (
fresh &&
headers["webhook-signature"].split(" ").some(entry => {
const signature = Buffer.from(entry.slice("v1,".length));
return (
entry.startsWith("v1,") &&
signature.length === expected.length &&
timingSafeEqual(signature, expected)
);
})
);
}Delivery
- Answer with a 2xx status within 15 seconds, then do the work. Anything else — an error status, a timeout, a redirect — counts as a failure.
- Failures are retried with exponential backoff for up to about 20 minutes, then dropped. Every retry has the same
webhook-id: use it to process each event once. - Order is not guaranteed. Compare
timestamp(ordata.updatedAt) before overwriting newer data with older. - Quick successive changes to the same row arrive as one
*.updatedcarrying the latest state, and a change that leavesdataas it was sends nothing. - Pausing or deleting a subscription also drops the deliveries still pending for it.