Webhooks for Apps
Ewity delivers signed webhooks to endpoints you register on your app's
Webhooks tab in the Developer Portal. You
can register any number of endpoints, and each endpoint subscribes to exactly
the events it wants — one endpoint for everything, or separate endpoints per
concern.
Per-event payloads live in the API reference under Ewity Apps API →
Webhooks. This page covers the delivery contract: the envelope, the
signature, and the event catalog.
The envelope
Every delivery is a POST with a JSON envelope:
{
"id": "evt_9f2c1a7d3b8e",
"event": "bill.created",
"created_at": "2026-08-12T14:03:22+05:00",
"company": { "id": 1, "name": "Test Company", "domain": "test" },
"install": { "store_id": "pl_id_rLCD8Ks1rVRGIFs", "external_account_id": null },
"data": { "…": "event-specific payload" }
}Headers on every delivery:
Content-Type: application/json
X-Ewity-Event: bill.created
X-Ewity-Delivery: evt_9f2c1a7d3b8e
X-Ewity-Signature: t=1786575678,v1=5f8a…c41d
id/X-Ewity-Deliveryis the delivery id — dedupe on it; retries
reuse it, so delivery is at-least-once.install.store_idtells you which merchant install the event belongs to —
the same value you send asX-Ewity-Storewhen calling the Apps API.- Respond with any
2xxquickly (aim for under 5 seconds) and do heavy work
asynchronously. Ewity spends at most 15 seconds on a delivery before
treating it as failed.
Retries & the health breaker
A failed delivery — non-2xx response, timeout, or connection failure — is
retried after 1, 4 and 8 minutes (four attempts total, same delivery id).
Endpoint health is tracked continuously, and it's visible per endpoint on the
Webhooks tab (delivered / failed / retried, hour by hour). An endpoint that
keeps failing — a majority of its deliveries over a sustained recent window,
with a minimum volume so one blip on a quiet endpoint never trips it — is
automatically disabled: deliveries stop, missed events are not queued, and
every developer on the app is emailed. Fix the endpoint, then press
Re-enable on the Webhooks tab; it gets a fresh health window. After
re-enabling, reconcile anything you missed through the Apps API.
Verifying the signature
Every delivery is signed with your app's signing secret (the whsec_…
value on the Webhooks tab):
X-Ewity-Signature: t=<unix timestamp>,v1=<hex hmac_sha256(secret, "{t}.{raw_body}")>
Verification, in any language:
- Read
tandv1from the header. - Compute
hmac_sha256(signing_secret, t + "." + raw_request_body)as hex. - Constant-time compare with
v1; reject on mismatch. - Reject if
|now − t|is more than 300 seconds (replay window).
const crypto = require("crypto");
function verify(req, secret) {
const m = String(req.headers["x-ewity-signature"] || "").match(/^t=(\d+),v1=([0-9a-f]{64})$/);
if (!m) return false;
const [, t, sig] = m;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${req.rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}Use the raw request body bytes — parsing and re-serialising the JSON will
break the signature.
Event catalog
| Event | Fires when |
|---|---|
app.installed | A business installed your app (payload includes approved roles). |
app.reauthorized | A business approved your updated role set. |
app.uninstalled | A business uninstalled your app. |
bill.created | A bill was created — sales, refunds and voids alike. |
bill.voided | A bill was voided — payload carries the void bill and its parent. |
bill.refunded | A bill was refunded — payload carries the refund bill and its parent. |
payment.created | A payment was recorded on an existing bill (credit settlements; settled_bill flags a cleared receivable). |
payment.reversed | A payment was reversed — payload links the reversal to the original payment. |
register_session.closed | A register session closed — the day-close trigger accounting syncs key on. |
expense.created / expense.updated | An expense was recorded / changed. |
customer.created / customer.updated | A customer record was created / changed. |
product.created / product.updated | A product was created / changed. |
quotation.created | A quotation was created. |
order.status_changed | An online order's status changed (full order payload). |
order.notification | A customer-facing message about an online order. |
order.review_reminder | An online order is waiting on customer review. |
store.turned_on | A previously offline online location came back. |
catalogue.updated | Catalogue/stock changed — re-fetch via the lookup endpoint. |
The sales-cycle events are designed so an accounting integration never has to
poll: bill.created carries each sale (with its at-sale payments),
payment.created / payment.reversed track receivables afterwards,
bill.voided / bill.refunded are the reversal moments, and
register_session.closed is the day-close signal to post the session's
takings (list its bills with GET /sales/bills/location/{location_id}?q_register_session_id=…).
Legacy webhooks (migrated ordering platforms)
Ordering platforms integrated before the Apps release used two fixed webhook
URLs with unwrapped payloads (X-Event header + your platform bearer
token, no envelope, no signature). Those endpoints were migrated automatically
and keep receiving byte-identical deliveries under the classic event names:
| Legacy event | Modern envelope twin |
|---|---|
OrderStatusChanged | order.status_changed |
OrderNotification | order.notification |
ReviewRequestedReminder | order.review_reminder |
StoreTurnedOn | store.turned_on |
CatalogueUpdated | catalogue.updated |
The twin events carry exactly the same data payload, wrapped in the
signed envelope. To upgrade, add a new endpoint subscribed to the modern
events, verify it in production, then delete the legacy endpoint. New
endpoints always use the envelope — the legacy format exists only for
migrated configurations.
Updated 14 days ago