Best practices
A short guide to integrating the Transfers and Wallets APIs in a way that survives retries and stays easy to reconcile.
Use an idempotency key for every write
Any POST, PUT, or PATCH that creates or updates a transfer or a wallet should carry an Idempotency-Key header. The API stores the first response it produced for that key for ~24 hours and replays it on every subsequent request with the same key, so retries become safe: if a network error or timeout leaves you unsure whether a transfer went through, sending the same request with the same key will either kick off the work (if the first attempt never reached us) or return the original response (if it did). Either way, you don't double-pay.
The key itself should be a value unique to the logical operation — a UUIDv4 works, or a deterministic hash of your own internal transaction ID. The important rules are:
- Same operation, same key. Every retry of the same request must reuse the same key. Generating a new UUID on each retry defeats the purpose.
- Different operation, different key. Do not reuse a key across two different payloads; the API will replay the original response rather than process the new payload.
- If you send a second request while the first is still being processed, you'll receive
409 idempotency_in_progress. Back off and retry with the same key.
POST /v2/batch_transfers
Idempotency-Key: 9b1a8c2e-2f2a-4c1e-8d2c-3d3a1e7c9b10
Content-Type: application/json
Authorization: Basic <base64(sk_live_xxx:)>Retry on network errors, timeouts, and 5xx responses using exponential backoff (e.g. 1s, 2s, 4s) with the same key. Do not retry on 4xx other than 409; those indicate a client-side problem that resending won't fix.
Pass your internal ID as reference_number
reference_numberEvery transfer accepts a reference_number field that PayMongo stores alongside the transfer and returns on reads. Use this to carry your canonical identifier — the payout ID, invoice number, payroll row ID, whatever you use to refer to the transaction in your own database.
This makes the two systems trivially reconcilable: you can query GET /v2/transfers?reference_number=<your-id> to find the PayMongo record for any internal transaction, and when support looks at a transfer, your reference travels with it.
A couple of things to keep in mind. The reference_number should be unique on your side — if you send the same value for two genuinely different transfers, you lose the 1:1 mapping. Keep it short and safe (no spaces, no special characters), and don't confuse it with provider_reference_number, which is the rail-issued number (InstaPay/PESONet) that appears asynchronously and is what you'll see on bank statements. The two are different fields and both are worth storing on your side:
id— PayMongo's transfer ID (e.g.tr_...). Canonical within PayMongo.reference_number— your ID. Set it on create.provider_reference_number— the rail's ID. Appears later, used for bank reconciliation.
{
"provider": "instapay",
"amount": 1500000,
"currency": "PHP",
"reference_number": "PAYROLL-2026-04-EMP-123",
"source_account": { "number": "...", "name": "...", "bic": "...", "bank_name": "..." },
"destination_account": { "number": "...", "name": "...", "bic": "...", "bank_name": "..." }
}Updated about 4 hours ago