Webhook introduction and configuration
The platform sends signed final order-status events as UTF-8 JSON to the HTTPS Webhook endpoints configured independently in Merchant Portal. An endpoint is not bound to an API Key and is not sent on each Payment or Payout creation request. Webhook requests carry Content-Type: application/json. This is an outbound Webhook delivery from the platform, not an ordinary merchant API endpoint.
Configuration and secret lifecycle
- Create one or more endpoints and select the event types for each endpoint. One event type can be delivered to multiple endpoints. New endpoints are disabled until explicitly enabled.
- On endpoint creation, the platform generates a Webhook Secret and Webhook Secret ID and displays the current Secret for 60 seconds. An authorized merchant administrator can reveal that current version again only after a fresh Google verification; historical, revoked, archived or operations-suspended versions cannot be revealed.
- Store the Secret in a server-side secret manager and deploy a receiver that verifies the raw Body before accepting events.
- Enable delivery only when the receiver can durably accept events. Return HTTP
200 OKin receiver examples; the platform treats any2xxresponse as successful receipt. - Changing the URL disables the endpoint and skips pending deliveries until the new receiver is reviewed and the endpoint is enabled again. Disable only this endpoint before rotating its Secret; API Key status is unrelated.
- Initial delivery and automatic retries use the URL and Secret version saved when that delivery record was created. A merchant or operations manual replay uses the endpoint's current URL and current Secret. Retain old Secret-ID mappings through the automatic retry window and select them strictly by
Webhook-Secret-Id.
Merchant Portal shows successful, failed and retrying deliveries and permits an authorized single-delivery retry. The platform also retries eligible failures automatically.
Request headers
These headers are notification-specific. They are sent only when the platform calls the merchant Webhook endpoint and are separate from the headers merchants send when calling ordinary APIs.
| Header | Example | Description |
|---|---|---|
Content-Type | application/json | Indicates that this Webhook Body is UTF-8 JSON. It is not included in the Webhook verification payload. |
Request-Id | req_fixture_000000000000000000000001 | Tracking ID for this delivery request. |
Webhook-Secret-Id | whsecid_fixture_001 | Webhook Secret identifier selected for this delivery. Use it to choose the locally stored Webhook Secret before verifying Signature. Merchant Portal shows this ID when the Secret is generated or rotated. |
Timestamp | 1780272000 | Unix delivery timestamp text in seconds. A retry may use a new timestamp. |
Nonce | nonce_fixture_webhook_001 | Delivery random string. A retry may use a new value. |
Signature | v1=<64 lowercase hex> | Versioned HMAC-SHA256 signature of the timestamp, random string and exact raw body. |
Header names are case-insensitive. The documentation shows canonical unprefixed Header names; do not add product, company or brand prefixes.
HTTP example
{merchantWebhookUrl} is the complete HTTPS notification receiving address configured by the merchant in the merchant portal. The domain and path are both defined by the merchant. Payment and Payout notifications can share one endpoint and be distinguished by Body eventType, or be routed to different endpoints by selecting different event subscriptions. The same event type may be subscribed by multiple endpoints.
Example merchant URL: https://merchant.example.com/.
POST {merchantWebhookUrl}
Content-Type: application/json
Request-Id: req_fixture_000000000000000000000001
Webhook-Secret-Id: whsecid_fixture_001
Timestamp: 1780272000
Nonce: nonce_fixture_webhook_001
Signature: v1=<64 lowercase hex>
{
"eventId": "evt_fixture_001",
"eventType": "PAYMENT_SUCCEEDED",
"merchantNo": "MCH_FIXTURE_001",
"order": {
"platOrderNo": "PHI112606010000000001001",
"merchantOrderNo": "DEMO_PAY_202606010001",
"status": "SUCCEEDED",
"productCode": "PH_PHP_PAYMENT_QRPH_GCASH",
"country": "PH",
"amount": {
"value": "100.50",
"currency": "PHP"
},
"referenceNo": "REF-20260601-0001",
"customData": {
"cartId": "CART-10001",
"customerRef": "CUST-90001"
},
"createTime": 1780272000,
"updateTime": 1780275600
}
}Signature verification
The signing payload has exactly three lines. Append one newline byte \n after every line, including the last line:
signingPayload = timestamp + "\n" + nonce + "\n" + rawBody + "\n"
signature = "v1=" + lowercase_hex(HMAC-SHA256(webhookSecret, signingPayload))Capture the raw body before JSON parsing, formatting or logging transformations. Check the timestamp against your configured policy, then select the exact locally stored Secret by Webhook-Secret-Id. Treat the Header as an untrusted selector into a local allowlist. Reject processing if the Secret ID is missing or unknown, and never try every historical Secret as a fallback.
Compare the expected and received signatures in constant time. Parse and process the JSON only after timestamp and signature verification succeeds.
WebhookEvent fields
| Field | Type | Required | Description |
|---|---|---|---|
eventId | string | Yes | Stable event ID used for deduplication. |
eventType | string | Yes | Stable final-status event type used to route business handling. |
merchantNo | string | Yes | Merchant number that owns the notified order. |
order | object | Yes | Final order status data carried by this Webhook. |
order.platOrderNo | string | Yes | Actual core platform order number for support and reconciliation. Treat it as opaque and do not infer the order type from its format. |
order.merchantOrderNo | string | Yes | Merchant-side order number used for business status update. |
order.status | string | Yes | Current final order status. |
order.failReason | string | No | Merchant-safe failure or cancellation reason. Returned when order.status is FAILED or CANCELED; omitted for SUCCEEDED. |
order.productCode | string | Yes | Product code used by the order. |
order.country | string | Yes | ISO 3166-1 alpha-2 country code. |
order.amount | object | Yes | Order amount object. |
order.amount.value | string | Yes | Amount value: transaction amount in major currency units; precision follows amount.currency and the selected productCode. |
order.amount.currency | string | Yes | Currency code: transaction currency, using ISO 4217 three-letter uppercase code. See supported countries and currencies. |
order.referenceNo | string | No | Upstream payment-network, payment-institution or recipient-institution reference number used to match payer receipts or recipient institution investigation results to the order. |
order.customData | object | No | Non-sensitive merchant-defined key-value pairs saved by the first accepted create request; idempotent duplicates never merge or overwrite them. |
order.recipientSummary | object | No | Masked recipient account summary. Returned for Payout notifications when available. |
order.recipientSummary.bankCode | string | Yes, when recipientSummary is returned | Recipient bank code. |
order.recipientSummary.accountName | string | Yes, when recipientSummary is returned | Recipient account holder name. |
order.recipientSummary.accountNoLast4 | string | Yes, when recipientSummary is returned | Last four characters of the recipient account number. |
order.createTime | timestamp | Yes | Unix timestamp in seconds, returned as a JSON number, when the order was created. |
order.updateTime | timestamp | Yes | Unix timestamp in seconds, returned as a JSON number, when the current notified order status became effective. This value is stable across Webhook retries. |
Timestamp in the Header is the timestamp of this HTTP delivery and can change on retry. It is sent as Header text. order.updateTime in the Body is a JSON number for the time when the notified order status became effective, and remains stable for the same event.
Body eventType uses this fixed event enum:
eventType | Meaning |
|---|---|
PAYMENT_SUCCEEDED | Payment order reached a successful final state. |
PAYMENT_CANCELED | Payment order expired or was closed before success. |
PAYOUT_SUCCEEDED | Payout order reached a successful final state. |
PAYOUT_FAILED | Payout order reached a failed final state. |
order.referenceNo is not the merchant order number, platform order number or channel order number. Use it only for payer receipt matching, recipient institution investigation and support. It is not used for ordinary lookup or idempotency.
Delivery handling
- Validate the signature, deduplicate by Body
eventId, persist the accepted event durably, and then return any HTTP2xxresponse as quickly as possible. No business response body is required. - The platform treats any
2xxresponse as successful receipt and stops retrying that delivery. Even if the order was already processed, return2xxafter idempotent handling to stop later retries. - Non-
2xxresponses, receiver timeouts, connection failures or TLS failures trigger automatic retry. The default retry window is 24 hours. Retries are approximately scheduled at 1m, 2m, 4m, 8m, 16m, 32m, and then about every 1h. Actual delivery time may vary and is not guaranteed to the exact second. Events still failing after the retry window enter manual compensation. - Deduplicate by Body
eventId;Request-Ididentifies one delivery request and is used only for troubleshooting. It must not be used as the business deduplication key. - Process accepted events asynchronously and keep business handling idempotent. Webhooks do not guarantee strict ordering; query by
merchantOrderNowhen a local state transition is unclear. - A retry may carry a new
Request-Id, delivery timestamp text, nonce and signature. BodyeventIdandorder.updateTimeremain the original event values. - Reject processing when
Webhook-Secret-Id, timestamp or signature verification fails. Do not parse or process the JSON payload before verification succeeds.
The machine schema is components.schemas.WebhookEvent in the OpenAPI Schema. See the Payment order webhook reference and Payout order webhook reference for rendered schemas and Java verification examples. Verify independent implementations with the provided Webhook signature test vectors.
