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

CallWhat it does
PATCH /webhooks/:idBody { 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/testBody { 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 /webhooksEach 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.

Wildcard: subscribe with "events": ["*"] to receive every event type, including ones added after you registered.

Event Types

EventSent when
document.createdA document is issued — manually, through the API, or by the recurring-billing cron.
document.status_changedA document's settlement changed: it was closed manually, covered by a new document, or settled by a receipt allocation.
document.allocatedA receipt was allocated against one or more invoices.
document.cancelledA receipt, credit note or supplier invoice was cancelled.
document.refundedA credit note (REFUND / RETURN) was issued against this document.
document.approval_number.assignedA Tax Authority allocation number was recorded on the document — including one completed after issue, when the document was first returned as PENDING.
charge.succeededA card charge was approved.
charge.failedA card charge was declined.
charge.capturedA previously authorized charge was captured.
charge.canceledAn authorization was voided.
refund.createdA refund was issued against a charge.
payment_method.attachedA card was saved for a customer.
payment_method.detachedA saved card was removed.
job_ticket.createdA job ticket was opened — by hand, from an order, or from a paid invoice.
job_ticket.step.startedAn employee started a step (at a station tablet, with their PIN) or a user started it in Glance.
job_ticket.step.pausedA step in progress was paused.
job_ticket.step.resumedA paused step was started again.
job_ticket.step.completedA step was finished. The ticket moves on to the next step.
job_ticket.step.skippedA step was skipped.
job_ticket.step.returnedThe ticket was sent back to an earlier step, with a note saying why.
job_ticket.movedThe ticket was dragged to another station on the board.
job_ticket.note_addedA note was added to the ticket.
job_ticket.cancelledThe ticket was cancelled — by a user, or because the invoice it was opened on was credited.
job_ticket.completedThe last step was finished; the whole ticket is done. Follows job_ticket.step.completed.
client.createdA client card was opened with POST /clients/create (in Glance or through the API), or a lead became a client.
client.updatedA client card was edited and at least one field changed. data.changedFields names the fields.
client.deactivatedA client was moved to the inactive list.
client.reactivatedAn inactive client was made active again.
client.mergedThis client card was merged into another one (POST /clients/merge). data.mergedInto names the card that stays.
client.deletedA 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.

No document contents: payloads never include line items, customer contact details, addresses or payment instrument data. Fetch 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
    }
  }
}
  • performedBy is { kind: "employee" | "user", id, name }, or null for 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.
  • sourceLineUuid is the document line the ticket was opened from. A store that sent its own line uuids (the WooCommerce plugin sends wc-<item id>) can map the ticket back to its order item.
Station filter: with 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"
  }
}
  • id is the client's visibleId, the one every clients endpoint takes; clientId is the numeric id that documents and their events carry as entityId; internalCode is the client code (קוד לקוח), or null.
  • changedFields (only on client.updated) lists the names of the fields that changed, never their values.
  • mergedInto (only on client.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/:visibleId with your API key when you need them.
  • Clients created on the fly by a document's client object do not send client.created.
Leads: with the CRM module, a card on an open or lost stage of the leads board is a lead, not a client yet, and sends no 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),
  );
}
Tip: compare with a constant-time function such as 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:

RetryDelay after the previous attempt
1st1 minute
2nd5 minutes
3rd30 minutes
4th2 hours
5th12 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.

Acknowledge first, process after: return 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.

Order is not guaranteed: a retried 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.

Last updated