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.

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.

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.

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