Partner Wallet Contract
When a customer confirms a purchase, Pagamio calls your wallet API. This page defines the standard contract Pagamio publishes. A partner whose API conforms needs configuration only (base URL, credential, amount unit and timings). A partner whose API differs can agree a per-partner mapping with Pagamio; the behaviour described here (hold, then capture or release, with idempotent retries) stays the same.
The Model
Funds move in three steps, keyed by one reference:
| Step | When | What your wallet does |
|---|---|---|
| Hold | Customer confirms; before the product is bought. | Reserve the amount on the customer's wallet so it cannot be spent elsewhere. |
| Capture | The product purchase succeeded. | Turn the hold into a final deduction of the held amount. |
| Release | The product purchase failed, or the outcome could not be confirmed as a success. | Free the held amount. |
Every call carries the reference: Pagamio's payment-leg identifier (a UUID) for that purchase. It is the same on the hold, the capture, the release and any retry, and it is your idempotency key.
Conventions
Base URL: your wallet base URL, agreed at onboarding. Paths below are relative to it.
Headers on every call
| Header | Value |
|---|---|
Authorization | ApiKey <credential> (the credential you give Pagamio at onboarding) |
Content-Type | application/json |
X-Correlation-Id | The payment-leg id. Log it; it lets both sides trace one purchase. |
Currency and amounts: currency is ZAR. By default amount is an integer in minor units (cents: R19.99 is 1999). If your API takes whole-currency decimals (19.99) tell Pagamio at onboarding and the unit will be configured for you. Amounts never have more than two decimal places.
Timeouts: Pagamio connects with a 5 second timeout and reads with a per-partner timeout (default 15 seconds). Answer quickly; a call that times out is treated as an unknown outcome (see Idempotency).
Hold
POST /holds
{
"reference": "b1f5a2c0-8a1e-4f0b-9d4e-2c6a1f7e9a10",
"customerId": "customer-123",
"walletId": "wallet-456",
"amount": 2500,
"currency": "ZAR",
"description": "Vodacom Airtime R25"
}
customerId and walletId are the values you supplied when you created the session (or, for a launch, the token's sub for both). description is the product name.
| Your response | Meaning to Pagamio |
|---|---|
201 with {"holdId": "...", "status": "HELD"} | Hold placed. Pagamio stores holdId and buys the product. |
409 with "status": "HELD" in the body | A hold for this reference already exists. Treated as success (an idempotent replay). |
409 with any other status | Genuine conflict. The purchase is declined. |
409 with no readable status | Unknown. Pagamio queries the hold by reference (see Query A Hold). It releases only a hold it can identify; otherwise the hold is left to expire at its TTL. |
422 with {"reason": "<REASON>"} | Definite refusal; see reasons. |
401 / 403 | Pagamio's credential was rejected. Purchase declined. |
404 | Declined. |
Any other 4xx | Declined. |
408, 429, any 5xx, or no answer in time | Unknown outcome. Pagamio queries the hold by reference. If it is HELD, the purchase continues. Otherwise the purchase is declined with a timeout message, and Pagamio releases only a hold it can identify; a hold it cannot identify is left to expire at its TTL. |
Decline Reasons
Return one of these reason values with a 422. They decide the message the customer sees.
reason | Customer sees |
|---|---|
INSUFFICIENT_FUNDS | "Insufficient funds in your wallet." |
CUSTOMER_NOT_FOUND | "We couldn't find a wallet for your account. Please contact our support." |
WALLET_BLOCKED | "Your wallet is not active. Please contact our support." (use this for an inactive, suspended or frozen wallet) |
LIMIT_EXCEEDED | "This purchase is over your wallet's limit." |
VALIDATION | "The purchase details were not accepted." |
AUTHENTICATION, DUPLICATE, or any other value | "Something went wrong. Please try again." |
Customer-facing wording is fixed by Pagamio. Text from your response is never shown to the customer.
Capture
POST /holds/{holdId}/capture
{
"reference": "b1f5a2c0-8a1e-4f0b-9d4e-2c6a1f7e9a10",
"amount": 2500,
"vasReference": "core-transaction-id",
"receiptSummary": "Vodacom Airtime R25"
}
Capture is for the full held amount (partial capture is not used). vasReference is Pagamio's transaction id for the product purchase, useful for your own reconciliation.
| Your response | Meaning to Pagamio |
|---|---|
200 with {"status": "CAPTURED"} | Done. If you return a captureId, Pagamio records it as your reference for the deduction; otherwise it records the hold id. |
409 with "status": "CAPTURED" | Already captured. Treated as success. |
409 with another or unreadable status | Declined (other status) or retried (unreadable). |
410 | The hold has expired or no longer exists. Escalated to Pagamio operations for manual resolution. |
Other definite refusals (401, 403, 404, 400...) | Escalated to Pagamio operations. |
408, 429, 5xx, timeout | Retried, see below. |
Release
POST /holds/{holdId}/release
{
"reference": "b1f5a2c0-8a1e-4f0b-9d4e-2c6a1f7e9a10",
"reason": "core_declined:AGGREGATOR_DECLINED"
}
reason is a short machine-readable note for your records.
| Your response | Meaning to Pagamio |
|---|---|
200 with {"status": "RELEASED"} | Done. An optional releaseId is recorded as your reference. |
409 with "status": "RELEASED" | Already released. Treated as success. |
| Other definite refusal | Escalated to Pagamio operations. |
408, 429, 5xx, timeout | Retried. |
Query A Hold
GET /holds?reference=<reference>
Pagamio uses this to resolve a hold whose response it never received. Answer as follows:
| Your response | Meaning to Pagamio |
|---|---|
200 with {"holdId": "...", "status": "HELD"} | The hold exists. The purchase continues, and Pagamio addresses later capture and release calls by this holdId. |
200 with "status": "CAPTURED" or "RELEASED" (including an expired hold) | Nothing to release. The purchase is declined with a timeout message and no release is sent. |
200 with any other status | A live hold in an unrecognised state. Pagamio releases it by its holdId and declines the purchase. |
404 | No hold exists under this reference. Pagamio sends no release. |
5xx, timeout, or a 200 with no readable status | Unknown. Pagamio releases only if it already knows the hold id; otherwise it sends nothing and the hold lapses at its TTL. |
A 200 must include holdId: it is how a later capture or release addresses the hold. Pagamio never sends a release without a hold id.
If you do not offer this endpoint, a hold whose response was lost is not released by Pagamio; it stays reserved on your side until it expires at its TTL. Implementing the query is therefore strongly recommended, and a hold lifetime that is not excessively long matters (while staying above the minimum in Timing Requirements). Tell Pagamio at onboarding if you cannot offer it.
Idempotency And Retries
- Same reference, same result. Repeating a hold, capture or release with the same reference must not move money twice. Return the current state instead (
409carrying thestatus, or the original success). - Pagamio retries unknown outcomes. Capture and release calls that time out or return
408,429or5xxare queued and retried with increasing delays: 30 seconds, 1 minute, 5 minutes, 15 minutes, then hourly. If they are still unresolved after 24 hours, the purchase is escalated to Pagamio operations for manual resolution. Retries reuse the same reference. - Definite refusals are not retried. A
4xxrefusal on capture or release is a decision, so it escalates straight away. - The hold call is retried only by way of the query. Pagamio does not blindly re-send a hold whose answer it lost: it queries first (see above), and only releases a hold it can identify.
- Return
2xxor a definite4xxpromptly. A slow5xxlooks the same as an outage.
Consistency With Pagamio's Records
Each business day Pagamio can give you a statement of every payment leg (see Transactions And Statements). Use it to reconcile your ledger against holds, captures and releases. Differences that persist are investigated with Pagamio operations using the reference and vasReference.
Requirements For Your Wallet API
- HTTPS with a valid certificate.
- A stable hold that remains valid for at least the period in Timing Requirements.
- One hold per reference. Reference reuse across customers is not expected.
- Idempotent hold, capture and release as above.
- A reachable sandbox that behaves like production.
Last updated: September 2026