Documentation menu

Outbound webhooks

Webhooks send Manisma events to a public HTTPS endpoint of your organization.

Configuring an endpoint

As an Admin, open Admin → Webhooks, create an endpoint, and choose a name, URL, and events. The signing secret is shown once. Store it safely. On rotation, the new secret applies from the moment of saving.

The URL must:

  • use HTTPS;
  • be publicly reachable via DNS;
  • use port 443 or 8443;
  • not contain a username/password in the URL;
  • not point to localhost, private IP, link-local, or cloud metadata.

Use Test to send a ping before activating real events.

Events

Event Trigger
ping Manual endpoint test.
client.created Client created.
client.updated Client modified.
job.created Job created.
job.planned Job status changed to planned.
job.completed Job status changed to completed.
job.cancelled Job status changed to cancelled.
approval.approved Client approved a proposal.
approval.declined Client declined a proposal.
workorder.completed Billing-ready work order data on completion.

A change to another field of an already completed job does not resend job.completed; the status itself must change.

Envelope and headers

{
  "event": "job.completed",
  "version": 1,
  "tenant_id": "tenant-uuid",
  "occurred_at": "2026-07-13T09:15:00.000Z",
  "data": {}
}

Headers:

  • X-Manisma-Event: event name;
  • X-Manisma-Delivery: stable id of this logical delivery;
  • X-Manisma-Signature: sha256=<hex HMAC-SHA256> over the exact raw body.

Verify the HMAC before JSON parsing and use a timing-safe comparison:

import { createHmac, timingSafeEqual } from 'node:crypto'

export function verify(rawBody, header, secret) {
  if (!header?.startsWith('sha256=')) return false
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
  const supplied = header.slice(7)
  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(supplied, 'hex')
  return a.length === b.length && timingSafeEqual(a, b)
}

Payloads

Client events contain id, customer_number, name, client_type, email, and phone. Internal client notes are absent.

Job events contain job id/number, title, status, priority, scheduling, and compact objects for client, service address, and lead technician. An unassigned or address-less job uses null.

Approval events contain the approval id, job_id, status, chosen slot, and response time. chosen_slot can be a JSON string or older free text; parse defensively.

workorder.completed contains:

  • compact job and client;
  • the most recent completed visit, if available;
  • used materials with SKU, sale price, quantity, and line amount;
  • work duration;
  • material total in EUR.

The material price is the sale price at the moment the webhook payload is built, not necessarily a historically locked price.

Delivery and retries

Webhooks are at least once. Make your receiver idempotent on X-Manisma-Delivery. A successful receipt is any 2xx response. Redirects are not followed and every attempt has a timeout of 10 seconds.

On a temporary error there are at most three attempts: immediately, after 60 seconds, and again 300 seconds later. The same delivery id is retained. Retries currently run in the service process and do not survive a redeploy; therefore also check the delivery log in Webhooks for leftover pending rows.

Save the delivery id first in the same transaction as your processing, then respond quickly with 2xx. Move slow processing to your own queue.