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 OK within 5 seconds.
  • If a 200 OK is 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

  1. In the dashboard, go to Settings → Webhooks → Create New.
  2. Subscribe to the e-wallet payment event (payment.debit.notify).
  3. Give the webhook a name and a target URL.
  4. On save, Durianpay sends a sample webhook to your URL. It must return 200 OK for the configuration to be saved.

2. Events After E-wallet Payment

Event NameTrigger
payment.debit.notifyFires 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 CodeStatus TextMeaning
00completedPayment successful; funds settle to your merchant account.
05cancelledPayment was cancelled before completion.Do not fulfill the order.
08expiredThe customer did not complete payment before it expired. Do not fulfill the order.
09rejectedThe payment is rejected, the fund will be refunded to the payor. Do not fulfill the order.

Top-level fields

FieldDescription
originalReferenceNoDurianpay's payment ID (e.g. pay_cd8JefLd1mz5433). Primary key for dedup and reconciliation. Matches referenceNo from Create eWallet Payment.
originalPartnerReferenceNoYour partnerReferenceNo from Create eWallet Payment.
latestTransactionStatusOutcome code (00 = completed).
transactionStatusDescHuman-readable status (e.g. completed).
amount.value / amount.currencyPayment amount and currency (e.g. "10000.00", "IDR").

additionalInfo fields

FieldDescription
isLivetrue for production, false for sandbox.
channelCodeE-wallet used for the payment (e.g. OVO).
paidTimeWhen the customer paid. 0001-01-01T00:00:00Z when not set.
finishedTimeWhen the payment reached its final status. 0001-01-01T00:00:00Z when not set.
updatedTimeLast status update time.
failureReasonFailed and rejected only. Object with code and message.
rejectionReasonRejected only. Why the payment was rejected (e.g. Payor Information Doesn't Match).
refundReferenceRejected only. ID of the automatic refund to the payor (e.g. rfn_W8QTj74Gil2042).
customerInfoCustomer details, see below.

customerInfo fields

FieldDescription
customer_idDurianpay customer ID (e.g. cus_AggggP7MSL3730).
customer_ref_idYour customer reference, if provided (may be empty).
emailCustomer email, if provided.
given_nameCustomer name, if provided (may be empty).
mobileCustomer mobile number, if provided.
❗️

Ensuring Callback Idempotency

Durianpay retries deliver the same payload. Deduplicate on originalReferenceNo:

  1. On first receipt, persist originalReferenceNo and process your business logic once.
  2. If the same originalReferenceNo arrives again, return 200 OK without reprocessing.

4. Digital Signature Verification

Every callback carries X-SIGNATURE and X-TIMESTAMP headers. Verify the signature before trusting the body.

Steps:

  1. Read X-SIGNATURE and X-TIMESTAMP from the request headers.

  2. 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.
  3. Verify with SHA-256 + RSA-2048 (PKCS#8) against Durianpay's public key.

  4. Return 200 OK only 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.isLive and 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)

{
  "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)

{
  "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"
}
responseCodeMeaningWhen to return
2005600SuccessfulCallback received and (idempotently) processed.
4015600Unauthorized, invalid signatureSignature verification failed.
5005600Internal server errorYour handler errored; Durianpay will retry.

Did this page help you?