FundPay

Signature generator

Sign requests with HMAC-SHA256, and verify the callbacks FundPay sends back. Try it live below, or copy a ready-made snippet in your language.

How it works

Five steps — the live tool and every snippet follow this exactly.

  1. 1Collect all request parameters (excluding signature).
  2. 2Sort the parameter keys alphabetically (A–Z).
  3. 3Form-url-encode into a key1=value1&key2=value2 string (space → +).
  4. 4Compute HMAC-SHA256 of that string with your merchant secret key.
  5. 5Send the lowercase-hex result as the signature field.
Deposit & withdrawal. Only non-empty fields are signed. A nested source_bank_account (withdrawal) is re-keyed to source_account_bank_code, source_account_no, source_account_name (note _no, not _number). The tool below applies this for you.

Live generator

Paste your params and secret key to compute a signature instantly.

Works for deposit and withdrawal — empty fields are excluded, and a nested source_bank_account is re-keyed to source_account_* (account_number → source_account_no).

Computed live in your browser with the Web Crypto API — the secret never leaves this page.

Signed fields (after re-keying)
Canonical string (sorted, form-url-encoded)
Signature (HMAC-SHA256, hex)

Code examples

Full API reference
# The sorted, form-url-encoded string of the fields you're signing:
STRING='amount=1000&backend_return_url=https%3A%2F%2Fexample.com%2Fwebhook&currency=THB&frontend_return_url=https%3A%2F%2Fexample.com%2Freturn&merchant_id=d0bf1184-c1c9-4101-b034-97e3b44edf4e&payment_method=bank_transfer&reference=ORD1234567&source_account_bank_code=kbank&source_account_name=TEST+HELLO&source_account_no=1234567&timestamp=2025-07-29T21%3A23%3A29%2B07%3A00'

# HMAC-SHA256 with your merchant secret key -> lowercase hex
SIGNATURE=$(printf '%s' "$STRING" | openssl dgst -sha256 -hmac "$MERCHANT_SECRET_KEY" | sed 's/^.* //')
echo "$SIGNATURE"

Callback verification

FundPay POSTs to your backend_return_url when a transaction settles. Verify the signature before trusting it. Deposits and withdrawals share one shape and one rule — transaction_type tells them apart, and status is approved, rejected, expired or pending.

Fields included in the signature

Exactly seven, sorted alphabetically — for deposits and withdrawals alike.

  • amount
  • merchant_id
  • reference_id
  • status
  • transaction_date
  • transaction_id
  • transaction_type
remark is not signed. A rejected callback carries an extra remark field explaining why — it is not part of the signature, and neither is signature itself. Approved and rejected callbacks are both signed over the same seven fields. Feed remark into the HMAC and every rejection will fail verification.
  • Sign the amount exactly as it arrives. 1500 signs as 1500, not 1500.00. Do not reformat or round it.
  • Percent-encode every value. transaction_date always contains colons, and reference_id may too — a reference like 10000001:20000002:30000003 must encode to 10000001%3A20000002%3A30000003.
  • Callbacks carry no timestamp. De-duplicate on transaction_id instead — delivery is at-least-once, and FundPay retries any non-2xx response.

Callback body

one tab per transaction_type
// approved
{
  "amount": 1234.56,
  "status": "approved",
  "signature": "0b661b18e4d54bb318a6548f87c438e7851c88dbe8badb5fe5ace4d5422e5f26",
  "merchant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "reference_id": "WD9322299",
  "transaction_id": "withdrawal_prd_EXAMPLE0000000000000000000000000",
  "transaction_date": "2026-01-15T09:30:00Z",
  "transaction_type": "withdrawal"
}

// rejected — carries "remark", which is NOT part of the signature
{
  "amount": 1500,
  "remark": "เลขบัญชีไม่ถูก",
  "status": "rejected",
  "signature": "89ea93e29d4d8d73ab35069174292f1b0ab9985faa0dddc4328aa79236b0dfec",
  "merchant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "reference_id": "WD9322300",
  "transaction_id": "withdrawal_prd_EXAMPLE1111111111111111111111111",
  "transaction_date": "2026-01-16T14:05:00Z",
  "transaction_type": "withdrawal"
}

Verify it

one verifier handles both
# Every value here is fictitious, signed with this example key so you can run it
# as-is and check your implementation against a known-good vector:
MERCHANT_SECRET_KEY=example_merchant_secret_key

verify() { # $1 = string to sign, $2 = signature from the callback body
  actual=$(printf '%s' "$1" | openssl dgst -sha256 -hmac "$MERCHANT_SECRET_KEY" | sed 's/^.* //')
  [ "$actual" = "$2" ] && echo "valid" || echo "INVALID"
}

# --- Withdrawal, rejected --------------------------------------------------
# The body also carries "remark": "เลขบัญชีไม่ถูก" - it is left out of the string.
verify \
  'amount=1500&merchant_id=3fa85f64-5717-4562-b3fc-2c963f66afa6&reference_id=WD9322300&status=rejected&transaction_date=2026-01-16T14%3A05%3A00Z&transaction_id=withdrawal_prd_EXAMPLE1111111111111111111111111&transaction_type=withdrawal' \
  '89ea93e29d4d8d73ab35069174292f1b0ab9985faa0dddc4328aa79236b0dfec'

# --- Deposit, approved -----------------------------------------------------
# Same seven fields. Note the colons in reference_id encoded as %3A.
verify \
  'amount=1000&merchant_id=3fa85f64-5717-4562-b3fc-2c963f66afa6&reference_id=10000001%3A20000002%3A30000003&status=approved&transaction_date=2026-01-15T09%3A30%3A00Z&transaction_id=deposit_prd_EXAMPLE0000000000000000000000000&transaction_type=deposit' \
  '55b3d2a9b728eac4399c9a051cfc0a48ad95fc458320e769de05504a54112541'