Webhooks
Webhooks push events to your server as they happen, so you do not have to poll for changes. Register an HTTPS endpoint, subscribe it to the events you care about, and Glance will POST a signed JSON payload to it every time one occurs.
Registering an Endpoint
Endpoints are managed under /webhooks. The same routes are also served under /grow/webhooks, which is a permanent alias kept for existing integrations.
POST /webhooks
Content-Type: application/json
{
"url": "https://billing.example.com/hooks/glance",
"events": ["document.created", "document.status_changed"]
}The response returns the signing secret. It is shown once, at creation — store it immediately; it is never returned again by any endpoint.
{
"success": true,
"id": "we_4f1c9a2e-0b6d-4f0a-9d5e-2c8b7a1f3e64",
"url": "https://billing.example.com/hooks/glance",
"events": [
"document.created",
"document.status_changed"
],
"secret": "whsec_2f9c1d4e7a0b3c6d9e2f5a8b1c4d7e0f3a6b9c2d5e8f1a4b"
}GET /webhooks lists your endpoints (without their secrets), and DELETE /webhooks/:id removes one. Managing endpoints requires an owner-level key, or a key with the webhooks.manage action role.
Editing, Pausing and Testing an Endpoint
| Call | What it does |
|---|---|
PATCH /webhooks/:id | Body { events?, isActive?, filters? } — change the subscribed events, pause (isActive: false) or resume the endpoint, or change its filter. The URL and the signing secret stay the same; a new URL is a new endpoint. |
POST /webhooks/:id/test | Body { type } — sends one signed sample of that event type to the endpoint right now and answers { delivered, error }. The sample carries livemode: false in data and is never retried. For job ticket types it is built from your latest real event of that type, and for client types from your newest client card, so tools like Make can learn the data structure. |
GET /webhooks | Each endpoint also returns filters and lastDelivery: { type, at, delivered, error } — the most recent delivery attempt. |
filters (on POST /webhooks or PATCH) narrows an endpoint beyond its event types. Today it takes { "jobStationIds": [3, 4] }: job ticket events only for steps at those stations. Omitted, null or an empty list means no filter. See job ticket events.
"events": ["*"] to receive every event type, including ones added after you registered.Event Types
| Event | Sent when |
|---|---|
document.created | A document is issued — manually, through the API, or by the recurring-billing cron. |
document.status_changed | A document's settlement changed: it was closed manually, covered by a new document, or settled by a receipt allocation. |
document.allocated | A receipt was allocated against one or more invoices. |
document.cancelled | A receipt, credit note or supplier invoice was cancelled. |
document.refunded | A credit note (REFUND / RETURN) was issued against this document. |
document.approval_number.assigned | A Tax Authority allocation number was recorded on the document — including one completed after issue, when the document was first returned as PENDING. |
charge.succeeded | A card charge was approved. |
charge.failed | A card charge was declined. |
charge.captured | A previously authorized charge was captured. |
charge.canceled | An authorization was voided. |
refund.created | A refund was issued against a charge. |
payment_method.attached | A card was saved for a customer. |
payment_method.detached | A saved card was removed. |
job_ticket.created | A job ticket was opened — by hand, from an order, or from a paid invoice. |
job_ticket.step.started | An employee started a step (at a station tablet, with their PIN) or a user started it in Glance. |
job_ticket.step.paused | A step in progress was paused. |
job_ticket.step.resumed | A paused step was started again. |
job_ticket.step.completed | A step was finished. The ticket moves on to the next step. |
job_ticket.step.skipped | A step was skipped. |
job_ticket.step.returned | The ticket was sent back to an earlier step, with a note saying why. |
job_ticket.moved | The ticket was dragged to another station on the board. |
job_ticket.note_added | A note was added to the ticket. |
job_ticket.cancelled | The ticket was cancelled — by a user, or because the invoice it was opened on was credited. |
job_ticket.completed | The last step was finished; the whole ticket is done. Follows job_ticket.step.completed. |
client.created | A client card was opened with POST /clients/create (in Glance or through the API), or a lead became a client. |
client.updated | A client card was edited and at least one field changed. data.changedFields names the fields. |
client.deactivated | A client was moved to the inactive list. |
client.reactivated | An inactive client was made active again. |
client.merged | This client card was merged into another one (POST /clients/merge). data.mergedInto names the card that stays. |
client.deleted | A client card was deleted for good. |
Payload
Every delivery has the same envelope. id identifies the event (shared across all endpoints it fans out to), created is a Unix timestamp in seconds, and data holds the event-specific body.
{
"id": "evt_7b1e4c9a-2d3f-4a5b-8c6d-9e0f1a2b3c4d",
"type": "document.status_changed",
"created": 1774428000,
"data": {
"document": {
"id": "d0f3a1c7-5b2e-4d8a-9f6c-1e3b5d7a9c2f",
"documentId": 84213,
"type": "INVOICE",
"number": 1042,
"externalId": "erp-9912",
"entityId": 5531,
"retainerId": null,
"issueDate": "2026-08-18T09:00:00.000Z",
"updatedAt": "2026-08-18T10:30:00.000Z",
"currency": "ILS",
"amount": 1000,
"tax": 180,
"totalWithTax": 1180,
"status": "PARTIALLY_PAID",
"isOpen": true,
"remainingAmount": 180,
"coveredAmount": 1000,
"taxAuthorityApprovalNumber": "31530001234567890",
"approvalNumberStatus": null,
"cancelled": false
},
"reason": "ALLOCATION",
"changedBy": "a91c5e2d-7f04-4b6a-8c3d-2e5f7a9b1c40"
}
}Document events always carry the document under data.document, with the fields above. id is the public document id used across the API and documentId is the internal numeric id that endpoints such as receipt allocation expect. externalId is the id you supplied when creating the document, so you can match the event to your own record without a lookup. Some events add fields next to document: allocations on document.allocated, cancellationReason on document.cancelled, refundedBy on document.refunded.
GET /documents/:id with your API key when you need the full document.Job Ticket Events
Companies with the job tickets (תיקי עבודה) module get one event per action in a ticket's log, sent after the action is saved. Each event describes the ticket as it is after the action. step is the step the action was on (null for ticket-level events); currentStep is where the ticket is now (null once every step is done). eventId is stable — use it to drop duplicates.
{
"id": "evt_0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
"type": "job_ticket.step.completed",
"created": 1792472400,
"data": {
"eventId": 8812,
"action": "COMPLETE",
"occurredAt": "2026-10-20T07:40:00.000Z",
"note": null,
"performedBy": {
"kind": "employee",
"id": 14,
"name": "דני כהן"
},
"ticket": {
"id": "job_6c1f…",
"ticketId": 412,
"number": 128,
"title": "כרטיסי ביקור",
"status": "IN_PROGRESS",
"priority": "NORMAL",
"scope": "LINE",
"sourceLineUuid": "wc-981",
"dueDate": "2026-10-22",
"quantity": 500,
"routeName": "דפוס דיגיטלי",
"clientId": 3301,
"product": {
"id": 77,
"name": "כרטיס ביקור"
},
"document": {
"id": "9b2e…",
"documentId": 30117,
"type": "INVOICE_RECEIPT",
"number": "30117"
},
"progress": {
"done": 2,
"total": 4
}
},
"step": {
"id": 9921,
"name": "הדפסה",
"stepType": "WORK",
"status": "DONE",
"position": 1,
"station": {
"id": 3,
"name": "דפוס"
},
"workedSeconds": 1260,
"returnCount": 0
},
"currentStep": {
"id": 9922,
"name": "חיתוך",
"stepType": "WORK",
"status": "READY",
"position": 2,
"station": {
"id": 4,
"name": "גימור"
},
"workedSeconds": 0,
"returnCount": 0
}
}
}performedByis{ kind: "employee" | "user", id, name }, ornullfor automatic actions (opening from an order, finishing the ticket).- Names are included so a message can be built without another call. The customer is only
clientId— no name, phone or e-mail. sourceLineUuidis the document line the ticket was opened from. A store that sent its own line uuids (the WooCommerce plugin sendswc-<item id>) can map the ticket back to its order item.
filters: { jobStationIds } an endpoint gets an event only when its station is in the list — the station of step, or of currentStep for a ticket-level event. An event that cannot be placed at a station (job_ticket.completed) is always sent, and the filter never affects document, client or charge events.Client Events
client.* events keep a CRM, a mailing list or a Make scenario in sync with your client cards without polling the clients list. Each one is sent after the change is saved, and describes the card as it is after the change (for client.deleted, as it was just before).
{
"id": "evt_3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"type": "client.updated",
"created": 1792483200,
"data": {
"id": "7c1a9e40-2b3d-4f8a-9c6e-1d5b7a3f2e08",
"clientId": 5531,
"internalCode": 104,
"isActive": true,
"changedFields": [
"phoneNumber",
"email"
],
"occurredAt": "2026-10-25T08:00:00.000Z"
}
}idis the client's visibleId, the one every clients endpoint takes;clientIdis the numeric id that documents and their events carry asentityId;internalCodeis the client code (קוד לקוח), ornull.changedFields(only onclient.updated) lists the names of the fields that changed, never their values.mergedInto(only onclient.merged) is{ id, clientId }of the card that stays. The event is sent for the duplicate, which is now inactive.- No name, phone, email or address — fetch
GET /clients/id/:visibleIdwith your API key when you need them. - Clients created on the fly by a document's
clientobject do not sendclient.created.
client.* events. client.created is sent once, when the lead becomes a client — a sale document (invoice, invoice-receipt, receipt or bill) is issued to it, or it is moved to a won stage or off the board. A card that was already announced and later put on the board keeps sending events as before.Verifying the Signature
Each request carries a Glance-Signature header:
Glance-Signature: t=1774428000,v1=5f8a...c31d
v1 is the HMAC-SHA256 of ${t}.${rawBody}, keyed with your endpoint secret. Compute it over the raw request body — re-serializing the parsed JSON changes the bytes and the signature will not match.
import crypto from "crypto";
function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=")),
);
const timestamp = Number(parts.t);
// Reject replays of an old, validly-signed delivery.
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1),
);
}crypto.timingSafeEqual, not ===.Delivery & Retries
Respond with any 2xx status to acknowledge a delivery. Anything else — including a timeout after 10 seconds — is treated as a failure and retried on this schedule:
| Retry | Delay after the previous attempt |
|---|---|
| 1st | 1 minute |
| 2nd | 5 minutes |
| 3rd | 30 minutes |
| 4th | 2 hours |
| 5th | 12 hours |
After the initial send and five retries the event is dropped. Each endpoint is retried independently: one failing endpoint never delays or drops deliveries to another.
200 as soon as you have stored the event and do the work asynchronously. Slow handlers are indistinguishable from failures once they cross the 10-second timeout, and will be redelivered.Idempotency on Your Side
Delivery is at least once. A handler that times out after doing its work, or a network failure on the response, both result in the same event arriving again — so your endpoint must be safe to call twice with the same body.
Deduplicate on the envelope's id: it is stable across every retry of the same event. Record processed ids and ignore one you have already seen.
document.created can arrive after the document.status_changed that followed it. Treat each payload as the state at created, and ignore an event older than what you have already recorded for that document.