POST before processing the payload.
Acknowledging webhooks
Your endpoint must return an HTTP 2xx response to acknowledge receipt. Return the response quickly and process the payload in a background job when possible. If your endpoint does not return a valid 2xx response, SingaPay retries delivery according to the rules in Webhook retry mechanism.Security mechanisms
IP Whitelist
Simplest approach. Restrict access to SingaPay’s official IP addresses at the firewall or app level.
Signature Validation
Recommended. Cryptographically verify every request is authentic and untampered using HMAC-SHA512.
Combined (Recommended)
Maximum security. Layer IP whitelisting first (fast rejection), then validate signatures (integrity).
| Approach | Best For |
|---|---|
| IP Whitelist only | Simple setup, testing environments |
| Signature only | Higher security, good for testing signature logic |
| IP Whitelist + Signature | Production — industry best practice |
| IP + Signature + Timestamp validation | High-security requirements with replay attack protection |
Option 1: IP Whitelist
Configure your firewall or application to only accept requests from SingaPay’s official IP addresses. Contact support or check your merchant dashboard for the official IP list.Option 2: Signature Validation
See How to validate signature below.Option 3: Combined (maximum security)
PHP
How to validate signature
Signature validation is optional but strongly recommended for all production environments.
X-Signature header is an HMAC-SHA512 signature generated by SingaPay. The ENDPOINT value in the string to sign must match the path and query string of your configured webhook URL exactly (for example /webhook/callback or /api/v1/webhooks/singapay).
Steps
Extract Headers
Pull these three values from the incoming request:
X-Signature— the signature to verifyX-Timestamp— Unix timestamp in secondsAuthorization— strip"Bearer "prefix to get the raw token
Get Your Client Secret
Retrieve your Client Secret from the merchant dashboard. This is your HMAC key.
Normalize and Hash the Request Body
- Parse the raw JSON body
- Sort all object keys recursively and alphabetically
- Re-encode with
JSON_UNESCAPED_UNICODEandJSON_UNESCAPED_SLASHES - Hash with SHA-256
Implementation examples
Best practices and troubleshooting
Do's
Do's
- Always validate signatures before processing any webhook
- Use constant-time comparison (
hash_equals,timingSafeEqual,compare_digest) to prevent timing attacks - Validate
X-Timestampis within 5 minutes of current time to block replay attacks - Use the raw request body for hashing — do not parse JSON first
- Store your Client Secret in environment variables or a secure vault
- Always use HTTPS for webhook endpoints
- Handle duplicate webhooks gracefully using stable IDs from the payload as idempotency keys
- Log all webhook requests (with signatures redacted) for audit purposes
Common mistakes
Common mistakes
- Not sorting JSON keys before hashing (order matters)
- Using SHA-256 for the HMAC instead of SHA-512 (body = SHA-256; signature = SHA-512)
- Wrong separator in string to sign (must be
:, not_or-) - Using your API Key instead of your Client Secret
- Parsing JSON before hashing (always use the raw body)
- Using simple string comparison instead of constant-time comparison
- Wrong field order — must be
METHOD:ENDPOINT:TOKEN:HASH:TIMESTAMP - Forgetting to strip
"Bearer "from the Authorization header - Not including query parameters in the endpoint path
Troubleshooting
Troubleshooting
If signature validation fails, check in order:
- String to sign format —
POST:ENDPOINT:ACCESS_TOKEN:HASHED_BODY:TIMESTAMP - JSON normalization — keys sorted recursively, correct encoding flags
- Hash algorithms — SHA-256 for body, SHA-512 for HMAC
- HMAC key — Client Secret, not API Key
- Endpoint path — exact match including query parameters
- Timestamp — valid Unix timestamp in seconds
- Access token — extracted from Authorization header, no
"Bearer "prefix - Debug logging — log
stringToSign,hashedBody,calculatedSignature(dev only)
Idempotency
SingaPay may deliver the same webhook more than once if your endpoint returned a non-2xx response and a retry was scheduled. Use a stable identifier from the payload (such astransaction_id, reff_no, or bill_number) to detect duplicates and skip reprocessing.
Return 200 OK only after you have safely recorded the event (or confirmed it was already processed).