Events & webhooks
Stop polling. Zopping emits events as things happen — orders, inventory, customers, products — so your system can react in near-real-time instead of scanning for changes.
How it works
- Zopping fires internal events on state changes (order created, status changed, stock updated, …).
- You enable an event integration for your organization — a template that routes a given event to a partner destination.
- Zopping pushes the event to that destination as it happens.
Webhooks
Zopping pushes an HTTP webhook to your configured endpoint the moment something changes. Each event carries the resource's identifier and only the fields that changed — call the API with that id when you need the full record.
The request you receive
Zopping delivers each event as an HTTP POST with a JSON body and these headers:
| Header | Meaning |
|---|---|
X-Event-Type | The event name (e.g. order.placed). |
X-Delivery-ID | Unique id for this delivery, reused on retries — use it to deduplicate. |
X-Event-Timestamp | When the event occurred — epoch milliseconds (e.g. 1754558096789). |
X-Webhook-Signature | Hex signature of the raw request body — verify it before trusting the payload. |
The X-Event-Type header tells you which event this is — the JSON body then
carries only the fields for that event (the payloads below).
Order Events
Every order event carries the order's id — its internal reference number,
always present — and, when you set one on the order, your own clientId (the
external order id). Match on whichever your system keys by. Each event carries
these plus only the fields that changed.
| Event | Fires when | Payload |
|---|---|---|
order.placed | An order is created. | id, clientId, status |
order.statusUpdated | The order moves to a new status. | id, clientId, status |
order.updated | One or more order details change in an edit — delivery address, delivery slot/date, fulfillment store, or order type. | id, clientId + only the fields that changed (addressId, slotId, preferredDate, storeId, type) |
order.itemUpdated | Order items change — quantity edits, additions or removals. | id, clientId, items[] of { itemId, oldQuantity, newQuantity } |
order.cancelled | The order is cancelled. | id, clientId |
Payload fields
order.updated always carries id (and clientId when set) and then only the keys that
changed in that edit — an edit that changes just the store sends only
storeId; an edit that changes the store and the type sends both.
| Field | Type | Notes |
|---|---|---|
id | string | The order's internal reference number — always present. |
clientId | string | Your own client id for the order (its external order id), present only when you set one. |
status | string | The order's status (e.g. PENDING, DISPATCHED). |
addressId | integer | The delivery address the order moved to. |
slotId | integer | The delivery slot the order moved to. |
preferredDate | string | The preferred delivery date the order moved to (YYYY-MM-DD). |
storeId | integer | The fulfillment store the order moved to. |
type | string | The order type the order moved to — DELIVERY or PICKUP. |
items | array | Objects of { itemId, oldQuantity, newQuantity } — all three integer. |
An added item comes through as oldQuantity: 0, a removed item as
newQuantity: 0.
Examples
// order.placed
{ "id": "1771", "clientId": "BLK-2026-002", "status": "PENDING" }
// order.statusUpdated
{ "id": "1771", "clientId": "BLK-2026-002", "status": "DISPATCHED" }
// order.updated — only the delivery address changed
{ "id": "1771", "clientId": "BLK-2026-002", "addressId": 90876 }
// order.updated — only the slot/date changed
{ "id": "1771", "clientId": "BLK-2026-002", "slotId": 44, "preferredDate": "2026-08-15" }
// order.updated — store and type changed in the same edit
{ "id": "1771", "clientId": "BLK-2026-002", "storeId": 7226, "type": "PICKUP" }
// order.itemUpdated
{ "id": "1771", "clientId": "BLK-2026-002", "items": [ { "itemId": 11111362279, "oldQuantity": 2, "newQuantity": 3 } ] }
// order.cancelled
{ "id": "1771", "clientId": "BLK-2026-002" }
Inventory Events
Every inventory event carries the same three identifiers. The event tells you which record changed, not what changed — fetch the item's current state from the inventory API and update from that. The same product in three stores is three separate records, so you get one event per store.
| Event | Fires when | Payload |
|---|---|---|
inventory.itemAdded | A product is stocked in a store for the first time. Never fires twice for the same product/store — a repeat write fires inventory.itemUpdated instead. | productId, storeId, sellerId |
inventory.itemUpdated | Any change to an existing item — stock count, MRP/discount, status (enabled / disabled / out-of-stock), unlimited-stock flag, tax, location. Admin edits, API writes and bulk CSV uploads all fire it (one event per row, so expect bursts). | productId, storeId, sellerId |
inventory.itemDeleted | An inventory record is deleted — the product is no longer stocked in that store. The identifiers refer to a record that no longer exists, so don't fetch it. Deleting the product from the catalogue does not fire this. | productId, storeId, sellerId |
Payload fields
| Field | Type | Notes |
|---|---|---|
productId | integer | The catalogue product. |
storeId | integer | Which store's inventory changed — the same product in multiple stores is a separate record per store. |
sellerId | integer | 0 = the organization's own inventory; non-zero = a marketplace seller's inventory for that product. |
Examples
// inventory.itemAdded — product 42 stocked in store 7 for the first time
{ "productId": 42, "storeId": 7, "sellerId": 0 }
// inventory.itemUpdated — stock or price of product 42 changed in store 7
{ "productId": 42, "storeId": 7, "sellerId": 0 }
// inventory.itemUpdated — a marketplace seller's inventory of product 91 changed
{ "productId": 91, "storeId": 7, "sellerId": 318 }
// inventory.itemDeleted — product 42 no longer stocked in store 7
{ "productId": 42, "storeId": 7, "sellerId": 0 }
Payment Events
Fires when money moves on an order — a rider collects cash, a payment settles, or a refund completes.
| Event | Fires when | Payload |
|---|---|---|
payment.riderCollected | A rider (delivery agent) collects COD at the doorstep on delivery. This is the collection moment — the order's payment_status may still be PENDING until the trip is settled. | orderId, orderClientId |
payment.completed | A payment on the order is settled — online capture (CUSTOMER), POS/admin register-payment (REGISTER_PAYMENT), or a rider trip's cash settled at fin-close (FIN_CLOSE). | orderId, orderClientId, collectedVia |
payment.refundCompleted | A refund on the order completes — online (gateway-confirmed) or a cash / wallet refund recorded by the merchant. Fires once, on the transition into COMPLETED. | orderId, orderClientId |
For a COD order collected by a rider you receive two events across its
lifecycle: payment.riderCollected at the doorstep, then payment.completed
(collectedVia: "FIN_CLOSE") when the trip's cash is settled.
Payload fields
| Field | Type | Notes |
|---|---|---|
orderId | string | The order's internal reference number — always present. |
orderClientId | string | Your own client id for the order (its external order id), present only when you set one. |
collectedVia | string | payment.completed only — how the payment was settled (FIN_CLOSE, REGISTER_PAYMENT, CUSTOMER). Not sent on payment.riderCollected (the event name already says how). |
The payloads are deliberately minimal; a consumer that needs the amount, method,
settlement state or gateway references calls the order API with the orderId.
payment.riderCollected≠ settled. It marks that the customer handed over cash, not that the order'spayment_statusisPAID. Treatpayment.completedas the settlement signal.
Examples
// payment.riderCollected — rider took the cash at the door
{ "orderId": "1771", "orderClientId": "BLK-2026-002" }
// payment.completed — trip cash settled at fin-close
{ "orderId": "1771", "orderClientId": "BLK-2026-002", "collectedVia": "FIN_CLOSE" }
// payment.refundCompleted
{ "orderId": "1771", "orderClientId": "BLK-2026-002" }
Customer Events
Every customer event carries the customerId — the identifier the customer
APIs use — plus only the fields that changed. Membership and address changes are
reported as customer events, because the customer is the record you look up to act
on them.
| Event | Fires when | Payload |
|---|---|---|
customer.created | A customer account is created — storefront signup, admin entry, guest conversion or import. | customerId |
customer.updated | One or more of the customer's details change in an edit — name, phone, email, profile image, your client id, or which address is default. Editing a phone number or email address directly fires this too. | customerId + only the fields that changed (name, firstName, lastName, phone, email, image, clientId, defaultAddressId) |
customer.statusUpdated | The customer is enabled, disabled or archived. | customerId, status |
customer.deleted | Deletion of the customer account is requested. See the note below. | customerId |
customer.membershipActivated | A membership becomes usable — bought outright, or once its payment clears. | customerId, membershipId |
customer.membershipExpired | A membership passes its end date. | customerId, membershipId |
customer.addressCreated | An address is added to the customer. | customerId, addressId |
customer.addressUpdated | One of the customer's addresses is changed. | customerId, addressId |
Payload fields
customer.updated always carries customerId and then only the keys that
changed in that edit — an edit that changes just the phone sends only
phone; an edit that changes the name and email sends both.
| Field | Type | Notes |
|---|---|---|
customerId | integer | The customer's id. |
status | string | The status the customer moved to — ENABLED, DISABLED or ARCHIVED. |
name | string | The customer's full name. |
firstName, lastName | string | The parts of the customer's name. |
phone | string | The customer's default phone number. |
email | string | The customer's default email address. |
image | string | Profile-image URL. |
clientId | string | Your own identifier for the customer. |
defaultAddressId | integer | The address that became the default. |
membershipId | integer | The membership the event concerns. |
addressId | integer | The address added or changed. |
A membership reports the two states you can act on: customer.membershipActivated
when it becomes usable and customer.membershipExpired when it lapses. Activation
covers both routes in — bought outright, or paid for online — so you never have to
watch for a purchase and an activation separately. Neither event carries a status,
because the event name is the status.
A status change does not appear in customer.updated —
customer.statusUpdated reports it instead, and an edit that changed nothing else
sends no customer.updated at all.
customer.updated is the only event that carries personal data, so verify
X-Webhook-Signature and serve your endpoint over HTTPS.
Customer tagging is not delivered over webhook; poll the customer if you need its tags.
customer.deletedfires on the delete request, not on the purge. The record enters a retention period and is removed later. Treat it as "this customer is going away", not "this customer is gone".
Examples
// customer.created
{ "customerId": 4821 }
// customer.updated — only the phone number changed
{ "customerId": 4821, "phone": "+919812345678" }
// customer.updated — name and email changed in the same edit
{ "customerId": 4821, "name": "A Bansal", "email": "a.bansal@example.com" }
// customer.updated — a different address became the default
{ "customerId": 4821, "defaultAddressId": 90876 }
// customer.statusUpdated
{ "customerId": 4821, "status": "DISABLED" }
// customer.deleted
{ "customerId": 4821 }
// customer.membershipActivated
{ "customerId": 4821, "membershipId": 12 }
// customer.membershipExpired
{ "customerId": 4821, "membershipId": 12 }
// customer.addressCreated
{ "customerId": 4821, "addressId": 90876 }
// customer.addressUpdated
{ "customerId": 4821, "addressId": 90876 }
Going further
- Receive & fulfill orders — the polling counterpart and the order lifecycle these events track.
- Extensions — some event flows depend on the relevant extension being enabled.