Push Mechanism (Webhooks)
Last Updated: 2026-07-02
Overview
Instead of constantly polling the API for changes, MallPlus Open Platform can push real-time notifications to your server via webhooks. When a subscribed event occurs (such as a new order or inventory change), MallPlus sends an HTTP POST request to your configured callback URL.
Webhook URLs must use HTTPS and respond with a 2xx status code within the 30-second timeout. SSRF protection blocks callback URLs that resolve to private or reserved IP ranges (RFC 1918, loopback, link-local, and cloud-metadata endpoints). Public tunneling services such as *.ngrok.io are not blocked and may be used to receive deliveries.
If delivery fails (non-2xx response, timeout, or TLS error), MallPlus retries up to 3 attempts with exponential backoff (4s, 16s, 64s). Configure event subscriptions in the Push Mechanism section of the console.
Event Types
The following 19 event types are available for subscription. Subscribe explicitly to each event you need — wildcards are not supported so every subscription is auditable.
| Event | Label |
|---|---|
| order.created | Order Created |
| order.updated | Order Updated |
| order.shipped | Order Shipped |
| order.cancelled | Order Cancelled |
| Event | Label |
|---|---|
| inventory.updated | Inventory Updated |
| Event | Label |
|---|---|
| product.created | Product Created |
| product.updated | Product Updated |
| product.deleted | Product Deleted |
| Event | Label |
|---|---|
| fulfillment.created | Fulfillment Created |
| fulfillment.updated | Fulfillment Updated |
| fulfillment.shipped | Fulfillment Shipped |
| Event | Label |
|---|---|
| order.status.updated | Order Status Updated |
| Event | Label |
|---|---|
| return.created | Return Created |
| return.approved | Return Approved |
| return.rejected | Return Rejected |
| return.disputed | Return Disputed |
| Event | Label |
|---|---|
| return.status.updated | Return Status Updated |
/open/v1/* without notice (see Versioning). Payload Format
All webhook payloads are JSON-encoded POST requests. Every delivery shares the same outer envelope — the data object is the only part that varies by event type. The maximum payload size is 64 KB (larger payloads are truncated before delivery).
{
"event": "order.shipped",
"eventId": "evt_01HK...",
"deliveryId": "del_01HK...",
"timestamp": "2026-06-30T10:30:00Z",
"data": {
"order_id": "ord_98765",
"tracking_number": "PHL123456",
"carrier": "lbc",
"shipped_at": "2026-06-30T10:30:00Z"
}
} The eventId is your idempotency key — it is stable across the fan-out to every subscriber and across retries. Dedup by this value so you never process the same logical event twice. The deliveryId is per-attempt and changes on each retry — useful for tracing a single attempt in your logs.
Delivery Headers
Every delivery carries these headers:
| Header | Value |
|---|---|
| Content-Type | application/json |
| X-MallPlus-Event | Same as the event field (e.g. order.shipped). Note: no -Type suffix. |
| X-MallPlus-Event-Id | Same as eventId — your idempotency / dedup key. |
| X-MallPlus-Delivery-Id | Per-attempt id — changes on each retry. |
| X-MallPlus-Timestamp | Unix epoch seconds when the delivery was generated. |
| X-MallPlus-Signature | HMAC-SHA256 hex digest — see Signature Verification below. |
Signature Verification
Every delivery is signed with your app's webhook secret using HMAC-SHA256. The signature is computed over the string <timestamp>:<event>:<raw-body> and sent as a plain hex digest in the X-MallPlus-Signature header. Always verify the signature before trusting the payload.
Capture the raw request body BEFORE any JSON middleware — re-stringifying parsed JSON can change byte ordering and cause signature mismatch (the most common gotcha). We also recommend rejecting deliveries whose timestamp is more than 5 minutes old.
import { createHmac, timingSafeEqual } from 'node:crypto'
const ts = headers['x-mallplus-timestamp']
const event = headers['x-mallplus-event']
const theirSig = headers['x-mallplus-signature']
const expected = createHmac('sha256', WEBHOOK_SECRET)
.update(`${ts}:${event}:${rawBodyString}`)
.digest('hex')
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(theirSig, 'hex')
const valid = a.length === b.length && timingSafeEqual(a, b)
if (!valid) return respond(401)
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return respond(401)Retries & Idempotency
Delivery is at-least-once. A 2xx response confirms the delivery and we never re-fire that event for that subscription. Any non-2xx response, timeout (30 s), or TLS failure triggers a retry — up to 3 attempts with exponential backoff (4s → 16s → 64s).
Because of at-least-once semantics, your handler MUST be idempotent. Dedup by the X-MallPlus-Event-Id header — the same id is reused across retries for one logical event. If all 3 attempts fail, the delivery is recorded as failed in the Push Log for inspection; it is not replayed automatically or manually — there is no self-service replay path, so make your handler robust to the retry sequence (4s, 16s, 64s) before it exhausts.
Delivery Logs
Every delivery attempt — successful or failed — is recorded in the console's Push Log, so you can debug failed deliveries, inspect payloads, and confirm timing without raising a support ticket. Both live and sandbox deliveries appear in the same log.
For the full Push Log reference — columns, filters, delivery-detail fields, result meanings, the 90-day retention window, and troubleshooting — see the dedicated Webhook Delivery Logs guide.
Sandbox Delivery
Sandbox webhook deliveries use the same dispatcher, signing, headers, and retry semantics as production. You can verify your signature-check code, idempotency-key handling, and retry handling end-to-end without ever touching a real seller. This is the recommended path for validating your integration before going live.
Drive a lifecycle through the sandbox simulation environment and watch the events fire to your callback URL. Sandbox requests must be signed with your sandbox secret (mp_*); live credentials (mp_live_*) cannot reach the sandbox surface. A typical end-to-end loop:
# 1. (one-time) Configure your webhook URL + subscribe to events in the console
# 2. Seed a fresh sandbox slate
curl -X POST https://sandbox.sandbox.open.mallplus.ph/open/v1/sandbox/seed \
-H "X-MallPlus-Partner-Id: $CLIENT_ID" \
-H "X-MallPlus-Timestamp: $TS" \
-H "X-MallPlus-Signature: $SIG"
# 3. Place a test order -> order.created fires
curl -X POST https://sandbox.sandbox.open.mallplus.ph/open/v1/sandbox/orders \
-H "Authorization: Bearer $SELLER_TOKEN" \
-d '{ "sellerId": "...", "buyerId": "...", "items": [...] }'
# 4. Ship via your app's normal flow -> order.shipped fires
curl -X POST https://sandbox.sandbox.open.mallplus.ph/open/v1/orders/$ORDER_ID/ship \
-d '{ "tracking_number": "TEST123", "carrier": "lbc" }'Every delivery is recorded in the Push Log (status, response time, attempt history). Failed deliveries are kept for inspection; they are not replayable. See Sandbox Testing for the full sandbox surface.