Skip to content

Signing and verification

Updated: 2026-09-16 21:49

The international API uses a tokenless server-side signing model. Ordinary API requests are signed by the merchant for the API service. Ordinary API responses are signed by the API service for the merchant. Webhook delivery requests are also signed by the platform for the merchant. The three flows use different directions, secrets and signing inputs, so implement them separately.

Merchant administrators or explicitly authorized technical users create API credentials in Merchant Portal → International Payments → API Access. The platform generates the API Secret and shows it only once in the current page session after creation. The platform does not accept custom Secrets. After a browser refresh or close, merchants, support personnel and PMS cannot view or recover that Secret again. Store it immediately in a server-side Vault, KMS or controlled environment variable.

For routine rotation, create a replacement Key, run old and new Keys in parallel, confirm cutover using last_used_at, disable the old Key for an observation period, and then revoke it permanently. If the current Secret is unavailable to the operator, or exposure is suspected, create a replacement Key and revoke the old Key.

Signing API requests

An ordinary API request is sent from the merchant backend to the API service. The merchant signs the HTTP request with the API Secret and puts the API Key, timestamp, nonce and signature into separate request Headers.

1. Prepare signing materials

MaterialDescription
API KeyMerchant API credential used to identify the merchant, Secret and permissions. Source access is controlled by the platform operations security group.
API SecretServer-side secret corresponding to the API Key. Use it only locally; never send it to the API service.
HTTP methodUppercase method, such as POST or GET.
Request URLPath plus original Query without scheme, host, port or fragment.
Request timestampUnix timestamp text in seconds, normally accepted within ±300 seconds of the API service server time.
Request noncenonce, unique for every request under the same API Key, maximum 128 characters.
Request bodyExact UTF-8 raw Body sent on the wire. Use an empty string for GET requests.

2. Get the HTTP method

Use the method of the actual API call and uppercase it:

text
POST

3. Get the request URL

The request URL contains only Path and Query. Do not include scheme, host, port or fragment. Use the exact Query order and encoding sent by the client; do not sort, decode or re-encode it.

text
/intl/v1/payment/order/create

Query example:

text
/intl/v1/payment/order/query?merchantOrderNo=DEMO_PAY_202606010001

For balance query, the request URL depends on the actual optional filters. If no country or currency filter is sent, sign:

text
/intl/v1/balance/query

If both filters are sent, sign the actual Query:

text
/intl/v1/balance/query?country=PH&currency=PHP

4. Generate the request timestamp

Use Unix seconds and send it as Header text. Keep merchant servers synchronized with NTP or an equivalent clock source. The API service rejects stale requests before business processing.

bash
date +%s

Example:

text
1780272000

5. Generate the request nonce

nonce reduces replay risk. Do not reuse it under the same API Key, including retries.

bash
openssl rand -hex 16

Example:

text
nonce_fixture_001

6. Get the request body

For POST requests, sign the exact raw JSON string sent on the wire. Do not change spacing, line breaks, field order or character encoding after signing. If a framework serializes JSON for you, sign the actual bytes it sends.

GET requests have no body. The fifth line is an empty string, and the final newline byte is still required.

7. Build the request signing payload

The request signing payload has exactly five lines. Append one newline byte \n after every line, including the last line. If a value already ends with \n, append the required newline anyway.

text
HTTP method\n
request URL\n
request timestamp text\n
request nonce\n
request body\n

Example:

text
POST\n
/intl/v1/payment/order/create\n
1780272000\n
nonce_fixture_001\n
{"merchantOrderNo":"DEMO_PAY_202606010001","productCode":"PH_PHP_PAYMENT_QRPH_GCASH","amount":{"value":"100.50"},"orderDescription":"Virtual order"}\n

8. Calculate the signature

Use the API Secret to calculate HMAC-SHA256 over the signing payload, encode the digest as lowercase hexadecimal, and prefix it with v1=.

bash
printf 'POST\n/intl/v1/payment/order/create\n1780272000\nnonce_fixture_001\n{"merchantOrderNo":"DEMO_PAY_202606010001","productCode":"PH_PHP_PAYMENT_QRPH_GCASH","amount":{"value":"100.50"},"orderDescription":"Virtual order"}\n' \
  | openssl dgst -sha256 -hmac "$INTL_API_SECRET" -hex

Signature format:

text
v1=<64 lowercase hex>

9. Build the request signing Headers

http
Api-Key: ik_live_xxx
Timestamp: 1780272000
Nonce: nonce_fixture_001
Signature: v1=<64 lowercase hex>

Notes:

  1. Never include the API Secret in request Headers.
  2. Timestamp, Nonce and Signature must match the signed values exactly.
  3. Ordinary API requests must also send standard HTTP media Headers: Accept: application/json; POST requests must also send Content-Type: application/json. These two Headers are not included in the signing payload.
  4. Request and response media formats are documented in Basic rules.
  5. Prefer the SDK. Custom implementations must pass the request-signature test vectors before launch.
  6. Header names are case-insensitive. The documentation shows canonical unprefixed Header names; do not add product, company or brand prefixes.

Verifying API responses

An ordinary API response is returned from the API service to the merchant. The API service signs the raw response Body with the API Secret corresponding to the request API Key. Merchants should verify the response before JSON parsing and business handling.

1. Read response headers

Ordinary API responses can include these headers:

HeaderReturned whenDescription
Content-TypeAll ordinary API responses with a JSON bodyFixed as application/json, indicating that the response Body is UTF-8 JSON. It is not included in the response verification payload.
Request-IdAll ordinary API responsesRequest tracking ID for support, log correlation and reconciliation.
TimestampOrdinary API responses where the API Key can be identifiedResponse signing timestamp text in Unix seconds.
NonceOrdinary API responses where the API Key can be identifiedResponse random string, unique for this response.
SignatureOrdinary API responses where the API Key can be identifiedv1= plus lowercase HMAC-SHA256 response signature.

Authentication-failure responses where the API service cannot identify the API Key, for example when Api-Key is missing or badly malformed, might not include signature headers. Keep Request-Id, HTTP status and error code for support correlation.

2. Check timestamp

Check that Timestamp is within the allowed clock skew. The recommended default is 300 seconds. Reject stale signed responses for business-state changes and record Request-Id for troubleshooting. The response signature algorithm is fixed as HMAC-SHA256 with the v1= signature prefix.

3. Capture the raw response body

Verification must use the exact raw Response Body. Do not verify a formatted, escaped, field-reordered or re-serialized JSON string. If the response body is empty, use an empty third line and still keep the final newline byte.

4. Build the response verification payload

The response verification payload has exactly three lines. Append one newline byte \n after every line, including the last line:

text
response timestamp text\n
response nonce\n
response body\n

Example:

text
1780272000\n
nonce_fixture_response_001\n
{"platOrderNo":"PHI112606010000000001001","status":"PROCESSING"}\n

5. Calculate and compare the signature

Use the API Secret corresponding to the request API Key to calculate HMAC-SHA256 over the verification payload, encode it as lowercase hexadecimal, add v1=, and compare it with the Signature header using a constant-time comparison.

If verification fails:

  1. Do not update the business final status from the response Body.
  2. Record environment, time, merchant order number, the Request-Id response Header, HTTP status and response Body error.code.
  3. Treat a verified HTTP 5xx create response or an unverifiable response as an unknown result: query by the original merchantOrderNo; if repeated queries still find no order, resend only the unchanged create request with that same order number.
  4. Check whether a proxy, gateway or framework dropped headers, changed the Body, decompressed it or re-encoded it.

Verifying Webhook requests

Webhook delivery is a platform to merchant HTTP request. In this section, "verification" means the merchant verifies the platform's Webhook request. The merchant's HTTP response to the platform is not signed.

1. Read Webhook request headers

Webhook requests use standalone delivery headers. The signature Header names remain consistent with ordinary API request and response signing:

HeaderDescription
Content-TypeFixed as application/json, indicating that the Webhook Body is UTF-8 JSON. It is not included in the Webhook verification payload.
Request-IdTracking ID for this delivery request.
Webhook-Secret-IdWebhook Secret identifier used for this delivery. Use it to choose the locally stored Webhook Secret before verifying Signature. Merchant Portal shows it when the Secret is generated or rotated.
TimestampDelivery signing timestamp text in Unix seconds.
NonceDelivery random string; retries can use a new value.
Signaturev1= plus lowercase HMAC-SHA256 notification signature.

2. Select the Webhook Secret

Use Webhook-Secret-Id to select the exact locally stored Webhook Secret. The merchant must save the mapping shown by Merchant Portal, for example whsecid_old_001 -> whsec_old and whsecid_new_002 -> whsec_new. Reject the delivery if the Secret ID is unknown, disabled or belongs to a different merchant environment.

3. Check timestamp and deduplication key

Check Timestamp before verification. The recommended clock-skew limit is 300 seconds. Retries can use new Request-Id, Timestamp, Nonce and Signature values, while Body eventId keeps the same event ID. Deduplicate by Body eventId. Header Timestamp is the delivery signing time and is sent as text; Body order.updateTime is the notified status effective time, is returned as a JSON number, and is stable for the same event.

4. Capture the raw Webhook body

Webhook verification must use the exact UTF-8 raw Body. Do not parse JSON, pretty-print JSON, replace line breaks or change field order before verification. Many frameworks consume the Body stream once, so cache raw bytes in middleware or a filter before parsing.

5. Build the Webhook verification payload

The Webhook verification payload has exactly three lines. Append one newline byte \n after every line, including the last line:

text
delivery timestamp text\n
delivery nonce\n
Webhook request body\n

Example:

text
1780272000\n
nonce_fixture_webhook_payment_001\n
{"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}}\n

6. Calculate and compare the signature

Use the Webhook Secret selected by Webhook-Secret-Id to calculate HMAC-SHA256 over the verification payload, encode it as lowercase hexadecimal, add v1=, and compare it with the Signature header using a constant-time comparison.

Only parse JSON after verification succeeds, and process the event idempotently by Body eventId. If verification fails, the Webhook Secret ID is unknown or the Body is modified, return 4xx or 5xx and do not update order state.

7. Acknowledge the delivery

Return any HTTP 2xx only after the event is durably stored or accepted into a reliable queue. No business response body is required. The platform treats any 2xx response as successful receipt; non-2xx responses, timeouts or network failures trigger retries. For long-running business processing, verify, deduplicate and persist first, then process asynchronously.