Skip to content

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: POST to /v2/webhook-subscriptions with a url and the events you want. The response carries the subscription's secret.

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.

ResourceEndpointAlso *.updated when
assignment/v2/assignmentsits address changes
assignment-upload/v2/assignments/{uuid}/uploads
assignment-event/v2/assignment-eventsusers 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/customersits billing address changes
gang/v2/gangsmembers join or leave
project/v2/projects
role/v2/roles
time-entry/v2/time-entriesits day is submitted, approved or declined, or its computed duration changes
time-entry-type/v2/time-entry-types
time-sheet-day/v2/time-sheet-daysits times are recalculated
time-sheet-week/v2/time-sheet-weeks
time-sheet-month/v2/time-sheet-months
upload-category/v2/upload-categoriesits roles change
user/v2/users
user-absence/v2/user-absencesits 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 (or data.updatedAt) before overwriting newer data with older.
  • Quick successive changes to the same row arrive as one *.updated carrying the latest state, and a change that leaves data as it was sends nothing.
  • Pausing or deleting a subscription also drops the deliveries still pending for it.