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-Delivery is the delivery id — dedupe on it; retries
    reuse it, so delivery is at-least-once.
  • install.store_id tells you which merchant install the event belongs to —
    the same value you send as X-Ewity-Store when calling the Apps API.
  • Respond with any 2xx quickly (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:

  1. Read t and v1 from the header.
  2. Compute hmac_sha256(signing_secret, t + "." + raw_request_body) as hex.
  3. Constant-time compare with v1; reject on mismatch.
  4. 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

EventFires when
app.installedA business installed your app (payload includes approved roles).
app.reauthorizedA business approved your updated role set.
app.uninstalledA business uninstalled your app.
bill.createdA bill was created — sales, refunds and voids alike.
bill.voidedA bill was voided — payload carries the void bill and its parent.
bill.refundedA bill was refunded — payload carries the refund bill and its parent.
payment.createdA payment was recorded on an existing bill (credit settlements; settled_bill flags a cleared receivable).
payment.reversedA payment was reversed — payload links the reversal to the original payment.
register_session.closedA register session closed — the day-close trigger accounting syncs key on.
expense.created / expense.updatedAn expense was recorded / changed.
customer.created / customer.updatedA customer record was created / changed.
product.created / product.updatedA product was created / changed.
quotation.createdA quotation was created.
order.status_changedAn online order's status changed (full order payload).
order.notificationA customer-facing message about an online order.
order.review_reminderAn online order is waiting on customer review.
store.turned_onA previously offline online location came back.
catalogue.updatedCatalogue/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 eventModern envelope twin
OrderStatusChangedorder.status_changed
OrderNotificationorder.notification
ReviewRequestedReminderorder.review_reminder
StoreTurnedOnstore.turned_on
CatalogueUpdatedcatalogue.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.


Did this page help you?