SNAP E-wallet Notification
When an e-wallet payment created with the SNAP Create eWallet Payment API reaches a final status, Durianpay sends a POST callback to your configured webhook URL with the payment result.
- Callbacks are delivered to the URL registered in your dashboard.
- Your endpoint must return
200 OKwithin 5 seconds. - If a
200 OKis not received in time, Durianpay retries at 2, 5, 10, 90, and 210 minutes after the original attempt. - Retries replay an identical payload, so your handler must be idempotent (see below).
Every e-wallet callback follows a Create eWallet Payment call. The callback's originalReferenceNo is the same Durianpay payment ID returned as referenceNo in that API response, so you can correlate the callback with the order you stored at creation.
1. Setting Up the Webhook
- In the dashboard, go to Settings → Webhooks → Create New.
- Subscribe to the e-wallet payment event (
payment.debit.notify). - Give the webhook a name and a target URL.
- On save, Durianpay sends a sample webhook to your URL. It must return
200 OKfor the configuration to be saved.
2. Events After E-wallet Payment
| Event Name | Trigger |
|---|---|
payment.debit.notify | Fires when an e-wallet payment reaches a final status: completed, cancelled, failed or expired. |
3. Handling the Callback
Read the outcome from the top-level latestTransactionStatus (mirrored in transactionStatusDesc):
| Status Code | Status Text | Meaning |
|---|---|---|
00 | completed | Payment successful; funds settle to your merchant account. |
05 | cancelled | Payment was cancelled before completion.Do not fulfill the order. |
08 | expired | The customer did not complete payment before it expired. Do not fulfill the order. |
09 | rejected | The payment is rejected, the fund will be refunded to the payor. Do not fulfill the order. |
Top-level fields
| Field | Description |
|---|---|
originalReferenceNo | Durianpay's payment ID (e.g. pay_cd8JefLd1mz5433). Primary key for dedup and reconciliation. Matches referenceNo from Create eWallet Payment. |
originalPartnerReferenceNo | Your partnerReferenceNo from Create eWallet Payment. |
latestTransactionStatus | Outcome code (00 = completed). |
transactionStatusDesc | Human-readable status (e.g. completed). |
amount.value / amount.currency | Payment amount and currency (e.g. "10000.00", "IDR"). |
additionalInfo fields
additionalInfo fields| Field | Description |
|---|---|
isLive | true for production, false for sandbox. |
channelCode | E-wallet used for the payment (e.g. OVO). |
paidTime | When the customer paid. 0001-01-01T00:00:00Z when not set. |
finishedTime | When the payment reached its final status. 0001-01-01T00:00:00Z when not set. |
updatedTime | Last status update time. |
failureReason | Failed and rejected only. Object with code and message. |
rejectionReason | Rejected only. Why the payment was rejected (e.g. Payor Information Doesn't Match). |
refundReference | Rejected only. ID of the automatic refund to the payor (e.g. rfn_W8QTj74Gil2042). |
customerInfo | Customer details, see below. |
customerInfo fields
customerInfo fields| Field | Description |
|---|---|
customer_id | Durianpay customer ID (e.g. cus_AggggP7MSL3730). |
customer_ref_id | Your customer reference, if provided (may be empty). |
email | Customer email, if provided. |
given_name | Customer name, if provided (may be empty). |
mobile | Customer mobile number, if provided. |
Ensuring Callback IdempotencyDurianpay retries deliver the same payload. Deduplicate on
originalReferenceNo:
- On first receipt, persist
originalReferenceNoand process your business logic once.- If the same
originalReferenceNoarrives again, return200 OKwithout reprocessing.
4. Digital Signature Verification
Every callback carries X-SIGNATURE and X-TIMESTAMP headers. Verify the signature before trusting the body.
Steps:
-
Read
X-SIGNATUREandX-TIMESTAMPfrom the request headers. -
Compose the string-to-sign in this format:
POST:<RELATIVE_PATH>:<LOWERCASE_HEX_SHA256_OF_MINIFIED_BODY>:<X-TIMESTAMP><RELATIVE_PATH>excludes the domain, e.g./v1.0/debit/notify.- Minify the JSON body (no insignificant whitespace), SHA-256 hash it, then hex-encode and lowercase.
<X-TIMESTAMP>is taken verbatim from the header.
-
Verify with SHA-256 + RSA-2048 (PKCS#8) against Durianpay's public key.
-
Return
200 OKonly if verification succeeds; otherwise reject (401).
Example string-to-sign:
POST:/v1.0/debit/notify:5d2c90ddfdd406117ced5c2b502c05b601d435c7e5440f82e58733fdd5f15b7d:2026-06-22T11:36:12+00:00
Sandbox and Live use different public keys. Use
additionalInfo.isLiveand your environment config to load the matching key.
Verification (Go)
package main
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
)
// VerifyEwalletCallback verifies the X-SIGNATURE of a Durianpay e-wallet callback.
func VerifyEwalletCallback(relativePath, timestamp string, body []byte, xSignature, publicKeyPEM string) error {
// 1. SHA-256 of the minified request body, hex-encoded lowercase.
bodyHash := sha256.Sum256(body)
bodyHashHex := hex.EncodeToString(bodyHash[:]) // hex.EncodeToString already lowercases
// 2. Compose the string-to-sign.
stringToSign := fmt.Sprintf("POST:%s:%s:%s", relativePath, bodyHashHex, timestamp)
// 3. Parse Durianpay's public key (PKIX / PEM).
block, _ := pem.Decode([]byte(publicKeyPEM))
if block == nil {
return errors.New("invalid public key PEM")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return err
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return errors.New("public key is not RSA")
}
// 4. Decode the base64 signature and verify (SHA-256, PKCS#1 v1.5).
sig, err := base64.StdEncoding.DecodeString(xSignature)
if err != nil {
return err
}
digest := sha256.Sum256([]byte(stringToSign))
return rsa.VerifyPKCS1v15(rsaPub, crypto.SHA256, digest[:], sig)
}5. Callback Payload Example
Completed payment (00)
00){
"amount": {
"value": "100.00",
"currency": "IDR"
},
"additionalInfo": {
"isLive": true,
"paidTime": "2026-10-26T06:52:43.092798678Z",
"channelCode": "OVO",
"updatedTime": "2026-10-26T06:52:43.095223Z",
"customerInfo": {
"email": "",
"mobile": "085722173217",
"given_name": "Jane Doe",
"customer_id": "cus_AggggP7MSL3730",
"customer_ref_id": ""
},
"finishedTime": "2026-10-26T06:52:43.092798678Z"
},
"originalReferenceNo": "pay_cd8JefLd1mz5433",
"transactionStatusDesc": "completed",
"latestTransactionStatus": "00",
"originalPartnerReferenceNo": "order-0001"
}Rejected payment (09)
09){
"amount": {
"value": "30.00",
"currency": "IDR"
},
"additionalInfo": {
"isLive": true,
"paidTime": "0001-01-01T00:00:00Z",
"channelCode": "OVO",
"updatedTime": "2026-10-22T10:20:34.972616Z",
"customerInfo": {
"email": "[email protected]",
"mobile": "085722173217",
"given_name": "Jane Doe",
"customer_id": "cus_sYceUF9mL09837",
"customer_ref_id": ""
},
"finishedTime": "0001-01-01T00:00:00Z",
"failureReason": {
"code": 20010,
"message": "Payor Information Doesn't Match"
},
"refundReference": "rfn_W8QTj74Gil2042",
"rejectionReason": "Payor Information Doesn't Match"
},
"originalReferenceNo": "pay_e3lIRrw9BY1503",
"transactionStatusDesc": "rejected",
"latestTransactionStatus": "09",
"originalPartnerReferenceNo": "order-0001"
}The payor's e-wallet has already been charged. Durianpay refunds it automatically; refundReference identifies that refund. Do not credit your customer.
6. Response Format
Acknowledge every callback with a minimal SNAP-shaped response. Only responseCode and responseMessage are expected. Do not echo request data or add custom fields.
{
"responseCode": "2005600",
"responseMessage": "Successful"
}| responseCode | Meaning | When to return |
|---|---|---|
2005600 | Successful | Callback received and (idempotently) processed. |
4015600 | Unauthorized, invalid signature | Signature verification failed. |
5005600 | Internal server error | Your handler errored; Durianpay will retry. |
Updated about 14 hours ago
