Getting Started > Push Mechanism

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.

Order events
EventLabel
order.createdOrder Created
order.updatedOrder Updated
order.shippedOrder Shipped
order.cancelledOrder Cancelled
Inventory event
EventLabel
inventory.updatedInventory Updated
Catalog events
EventLabel
product.createdProduct Created
product.updatedProduct Updated
product.deletedProduct Deleted
Fulfillment events
EventLabel
fulfillment.createdFulfillment Created
fulfillment.updatedFulfillment Updated
fulfillment.shippedFulfillment Shipped
Order Simulator events (Sandbox)
EventLabel
order.status.updatedOrder Status Updated
Return events
EventLabel
return.createdReturn Created
return.approvedReturn Approved
return.rejectedReturn Rejected
return.disputedReturn Disputed
Return Simulator events (Sandbox)
EventLabel
return.status.updatedReturn Status Updated
Forward compatibility: Your endpoint MUST treat unknown event types as no-ops — log and ignore, do not crash. MallPlus may add new event types within /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:

HeaderValue
Content-Typeapplication/json
X-MallPlus-Event Same as the event field (e.g. order.shipped). Note: no -Type suffix.
X-MallPlus-Event-IdSame as eventId — your idempotency / dedup key.
X-MallPlus-Delivery-IdPer-attempt id — changes on each retry.
X-MallPlus-TimestampUnix epoch seconds when the delivery was generated.
X-MallPlus-SignatureHMAC-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.

Pre-GA payloads: Order-event webhook payload shapes are pre-GA and documented as subject to change when the Orders API is released. Do not hardcode business logic against Alpha payloads — treat fields as illustrative and version your parser defensively.