Scope
The complete Server-to-Server flow reference. Every payment scenario, the purchase lifecycle, tokenisation, webhooks and the operations available after authorisation.
If you have not yet completed a test transaction, start with the Integration quick start — it walks the six requests end to end. This page assumes that works and goes deeper.
Authentication and keys
| Key | Used for |
|---|---|
| Bearer token | Every API operation: clients, purchases, charge, capture, release, refund, cancel, webhooks, reporting |
| S2S token | Submitting card data to a purchase’s direct_post_url |
The S2S token is not on your dashboard. Only the bearer token is. The S2S token is issued to qualifying merchants by Precium, because submitting clear PAN data puts you in PCI DSS SAQ-D scope. If you do not have one, you are not provisioned for the full server-to-server flow; speak to your account contact before building against it.
Both are bearer tokens on the Authorization header. Creating a purchase additionally requires your brand_id. The key also selects the environment — test and production share one base URL, and test data is fully isolated from live data.
Transaction classification
Every flow below is one of three classes. The class determines whether 3D Secure is presented, whether a CVC is required, and where chargeback liability sits.
| Class | Cardholder | 3D Secure | CVC | Liability |
|---|---|---|---|---|
| CIT | Present | Required | Required | Shifts to issuer on successful authentication |
| CIT → MIT | Present for the first payment | Required on the first payment | Required on the first payment | Shifts on the authenticated first payment |
| MIT | Not present | Not presented | Not used — may be stubbed | Rests on the original authentication being replayed correctly |
In South Africa the first charge against a card must always be authenticated. Subsequent merchant-initiated charges against the resulting token do not present a challenge.
Purchase lifecycle
A purchase moves through a defined set of states. Read status to know where you are; never infer it from a browser redirect.
| Status | Meaning |
|---|---|
created | Created, awaiting payment |
sent / viewed | Invoice sent / checkout opened |
pending_charge | Authorisation in flight |
hold | Authorised, funds reserved, awaiting capture or release |
preauthorized | Card verified with no financial transaction |
pending_capture / pending_release / pending_refund | Operation accepted, acquirer still processing |
paid | Captured and complete |
released | Hold released without capture |
refunded | Refunded in full |
cancelled | Cancelled before payment |
chargeback | Disputed by the cardholder |
expired / overdue | Payment window elapsed / past its due date |
error | Failed — inspect transaction_data.attempts[], newest first |
Flow 1 — Regular redirect (CIT)
Create the purchase and send the customer to checkout_url. Precium hosts the card form, so you handle no card data.
POST https://gate.reviopay.com/api/v1/purchases/
{
"client_id": "00000000-0000-4000-8000-000000000001",
"brand_id": "00000000-0000-4000-8000-000000000002",
"purchase": { "currency": "ZAR", "products": [ { "name": "Order 1042", "price": 149900 } ] },
"success_redirect": "https://www.example.com/success",
"failure_redirect": "https://www.example.com/failure"
}Flow 2 — Direct Post (CIT)
Your own form, posted straight to the purchase’s direct_post_url. Card data never reaches your servers, so your PCI scope is raised only to SAQ A-EP.
The form takes six fields: pm, cardholder_name, card_number, expires (MM/YY), cvc, and remember_card. Validate them before submitting — a format error is returned as a payment failure, not a validation error.
<form method='POST' action='{direct_post_url}'>
<input type='hidden' name='pm' value='visa'>
<input name='cardholder_name' maxlength='30' required>
<input name='card_number' maxlength='19' required>
<input name='expires' maxlength='5' placeholder='MM/YY' required>
<input name='cvc' maxlength='4' required>
<input type='checkbox' name='remember_card' value='on'>
<button type='submit'>Pay</button>
</form>For server-side submission, post to {direct_post_url}?s2s=true as JSON using
your S2S token. That is the full S2S flow and requires SAQ-D.
A server-side submission needs more than the six form fields. In a browser form the browser supplies its own context; posting server to server, you must supply it yourself, and 3D Secure risk assessment depends on it:
remote_ip, user_agent, accept_header, language, java_enabled,
javascript_enabled, color_depth, utc_offset, screen_width,
screen_height.
See the quick start for a complete request.
The Direct Post response
Whatever you post to direct_post_url, the HTTP status is 200. The
outcome is in the status field of the body, and there are two shapes.
The card is not enrolled in 3D Secure. status is one of:
status | Meaning |
|---|---|
executed | Authorisation and capture completed |
authorized | Funds reserved, because skip_capture was requested |
error | The authorisation failed |
The card is enrolled in 3D Secure. status is 3DS_required and the body
carries the fields needed to hand the customer to their bank.
Do not assume 3DS_required. A card that is not enrolled returns a financial
status directly from this same call, so branch on status rather than
expecting an authentication step. South African cards are expected to be
enrolled, since 3DS is mandated locally; a non-enrolled card is more likely to
be foreign-issued.
Completing a 3D Secure authentication
| Field | What it is |
|---|---|
Method | GET or POST. The method the browser must use. These are the only two values |
URL | The issuer’s Access Control Server. Not a Precium URL |
PaReq | Pass through unchanged |
MD | Pass through unchanged. May arrive empty — send it on as an empty string |
- Send the customer’s browser to
URLusingMethod, carryingPaReqandMDas you received them, plus aTermUrlof your own. Use query parameters forGETand body parameters forPOST. - The issuer presents its own authentication page. It is branded by the bank; you do not recreate it. It can take several seconds to appear, which is the issuer responding rather than a delay inside Precium.
- On completion the issuer redirects the browser by
POSTto yourTermUrl, withMDandPaResin the request body. - Send
MDandPaResback to Precium from your server to complete the authorisation. This call takes no authentication headers. - Confirm the final outcome from the webhook, not from this response.
A frictionless authentication is treated exactly like a completed challenge: 3DS was invoked, the issuer judged the transaction low risk and did not show the customer a challenge. Your integration does nothing differently.
Routing through a 3DS Brand invokes 3DS on every customer-initiated transaction, but the issuer still decides whether the customer sees a challenge.
A recurring or token charge does not run a new 3DS challenge. It uses the credential established by the original transaction, and it does not carry a fresh liability shift.
Flow 3 — Preauthorisation (card verification)
Verify a card and store it without moving money. Set skip_capture: trueand a purchase total of zero.
POST https://gate.reviopay.com/api/v1/purchases/
{
"client_id": "00000000-0000-4000-8000-000000000001",
"brand_id": "00000000-0000-4000-8000-000000000002",
"purchase": { "currency": "ZAR", "products": [ { "name": "Card verification", "price": 0 } ] },
"skip_capture": true,
"force_recurring": true,
"success_redirect": "https://www.example.com/success",
"failure_redirect": "https://www.example.com/failure"
}The result is status: preauthorized. Only cardholder verification happens — no funds are reserved and no financial transaction occurs. The card is stored, so the purchase id becomes a token for later merchant-initiated charges.
3D Secure is still required. A zero-amount authorisation is authenticated like any other customer-initiated payment.
Not every issuer answers a zero-amount request. Try zero first. If the
issuer times out or declines it, fall back to a ZAR 1.00 authorisation and
call release once the card has been stored. The card is saved either way.
Amount limits
Amounts are sent in cents: ZAR 1.00 is 100. This applies in test as well
as production.
| Minimum | No API minimum, but South African banking rails will not settle a card amount below ZAR 1.00. Use at least 100 for anything that must settle, including live testing |
| Maximum | No universal maximum. The practical ceiling comes from the merchant configuration, the issuer, cardholder limits and fraud controls. Agree high-value use cases during onboarding |
Reconciliation files are the exception: amounts there are in major units with four decimal places, not cents. See Reconciliation files.
Purchase due time
A purchase carries a due time. If you do not supply one, it defaults to one
hour. Once that window elapses the purchase moves to overdue.
overdue is not a decline. It means the payment window passed, and by default
payment is still possible afterwards. Set purchase.due_strict to true to
refuse payment once due has passed.
Flow 4 — Delayed capture (hold funds)
Reserve funds now and settle later — deposits, hotel bookings, car hire, anything where the final amount is not yet known. Set skip_capture: true with a non-zero total.
The purchase reaches status: hold. Resolve it one of two ways.
Capture
POST https://gate.reviopay.com/api/v1/purchases/{id}/capture/
{ "amount": 120000 }amount is optional and in cents. Omit it to capture the full authorisation. Capture less and the remainder is released automatically — you cannot capture the same authorisation twice, so a partial capture closes it.
Release
POST https://gate.reviopay.com/api/v1/purchases/{id}/release/Releases the hold without taking payment.
Both are asynchronous
If the acquirer is slow you get HTTP 200 with status: pending_capture or pending_release, plus a matching webhook. The outcome then arrives as purchase.captured (status: paid) or purchase.released (status: released).
On failure you get HTTP 400 with purchase_capture_error or purchase_release_error. For the reason, GET /purchases/{id}/ and read transaction_data.attempts[] — newest first — where .error carries the code and description.
Holds expire. Card networks typically release an uncaptured authorisation within 7 to 30 days, so capture or release deliberately rather than letting it lapse.
Flow 5 — Tokenisation and recurring charges (CIT → MIT)
Store a card on an authenticated first payment, then charge it later without the cardholder present.
Store the card
Pass remember_card=on to the direct_post_url, or force_recurring: true on the purchase. On success the purchase’s own id becomes the card token, and that purchase carries is_recurring_token: true.
Charge the stored card
Create a new purchase for the new amount, then charge it against the original:
POST https://gate.reviopay.com/api/v1/purchases/{new_purchase_id}/charge/
{ "recurring_token": "{original_purchase_id}" }HTTP 200 means the new purchase is paid. Use the same recurring_token for every subsequent charge — it does not rotate.
List and delete tokens
GET /clients/{id}/recurring_tokens/
DELETE https://gate.reviopay.com/api/v1/clients/{id}/recurring_tokens/{token_id}/
POST /purchases/{original_purchase_id}/delete_recurring_token/Deleting via the purchase resets its is_recurring_token to false. Give customers a way to remove a stored card; you will need it.
Flow 6 — Merchant-initiated charges (MIT)
For a charge with no cardholder present, flag it as recurring and reference the prior transaction so the issuer can link the two.
"payment_method_details": {
"card": {
"is_recurring": true,
"previous_network_transaction_id": "000000000000000",
"original_amount_cents": 10000
}
}| Field | Notes |
|---|---|
is_recurring | Boolean. Marks the transaction merchant-initiated |
previous_network_transaction_id | From the original authenticated transaction. Ask your account manager if you do not hold it |
original_amount_cents | Integer, in cents. The amount of the original deduction |
CVC is not used for merchant-initiated transactions and may be stubbed. Omitting the linking fields is permitted where you are approved for non-3DS processing, but it measurably affects issuer acceptance and moves chargeback exposure to you.
payment_method_details
Flows 7 and 8 both send a payment_method_details object. It nests inside
purchase, alongside currency and products, not at the top level of the
request.
payment_method_details is a bespoke extension for the server-to-server
flow. It is deliberately not part of the published OpenAPI schema and will not
appear in the generated reference or in the Swagger
page. The fields below are the contract; treat this page as their source.
Flow 7 — External MPI (your own 3D Secure)
If you run your own Merchant Plug-In, authenticate first and pass the results on the purchase.
"purchase": {
"currency": "ZAR",
"products": [ { "name": "My product or service", "price": 10000 } ],
"payment_method_details": {
"card": {
"is_external_3DS": true,
"authentication_transaction_id": "your-mpi-transaction-id",
"cavv": "base64-cardholder-authentication-value",
"eci_raw": "05",
"xid": "3ds1-transaction-identifier",
"is_recurring": true,
"previous_network_transaction_id": "016153570198200"
}
}
}| Field | Required | Notes |
|---|---|---|
is_external_3DS | Yes | Must be true |
authentication_transaction_id | Yes | Your MPI’s identifier for the authentication |
cavv | Yes | Cardholder Authentication Verification Value |
xid | 3DS1 only | Transaction identifier |
eci_raw | Yes | Electronic Commerce Indicator — see below |
is_recurring | For MIT | Flags the charge as merchant-initiated |
previous_network_transaction_id | For MIT | Links this charge to the transaction that established the credential |
ECI values
| ECI | Meaning | Liability shift |
|---|---|---|
05 | Fully authenticated (Visa) | Yes |
02 | Fully authenticated (Mastercard) | Yes |
06 | Authentication attempted (Visa) | Conditional |
01 | Authentication attempted (Mastercard) | Conditional |
07 | Not authenticated (Visa) | No |
00 | Not authenticated (Mastercard) | No |
This flow must run against a brand configured for external 3D Secure. Contact your account manager before building it.
Flow 8 — External network tokens
If you provision your own network tokens from Visa Token Service or Mastercard
Digital Enablement Service, present the token and its cryptogram in place of a
PAN. The token goes in number, exactly where a PAN would.
"purchase": {
"currency": "ZAR",
"products": [ { "name": "My product or service", "price": 10000 } ],
"payment_method_details": {
"card": {
"number": "4567861893990640",
"cryptogram": "dynamic-cryptogram-from-the-scheme",
"auth_brand": "MASTERCARD",
"card_type": "001",
"transaction_type": "3",
"xid": "",
"is_recurring": true,
"previous_network_transaction_id": "016153570198200",
"original_amount_cents": "10000"
}
}
}| Field | Required | Notes |
|---|---|---|
number | Yes | The network token, in place of the PAN |
cryptogram | Yes | The dynamic cryptogram issued with the token |
auth_brand | Yes | VISA or MASTERCARD |
card_type | Yes | Scheme card type code, for example 001 |
transaction_type | Yes | Scheme transaction type code, for example 3 |
xid | 3DS1 only | Send an empty string when not applicable |
is_recurring | For MIT | Flags the charge as merchant-initiated |
previous_network_transaction_id | For MIT | Links this charge to the transaction that established the credential |
original_amount_cents | For MIT | The amount of the original transaction, in cents |
Network tokens generally improve authorisation rates and remove the need to handle card expiry updates. Use a non-3DS Brand for merchant-initiated charges on this flow.
Restricting payment methods
Use payment_method_whitelist to limit what a purchase will accept. Some capabilities require exactly one method, so set it deliberately rather than leaving it open.
"payment_method_whitelist": ["visa", "mastercard", "maestro"]Card methods include visa, mastercard, maestro, american_express and diners_club. The gateway also carries non-card methods — instant EFT, DebiCheck, debit orders and a range of pan-African rails — which are documented under Payment Orchestration.
Instant EFT has two providers, and the identifier is the only thing that differs:
| Provider | payment_method_whitelist value |
|---|---|
| Ozow | ozow |
| OneGate | onegate_eft_redirect |
Rather than hard-coding a list, ask the API what a brand actually has enabled:
GET https://gate.reviopay.com/api/v1/payment_methods/?brand_id={brand_id}¤cy=ZARSend it with the same key you will create the purchase with — the key determines whether the lookup runs against test or live configuration.
Refunds
POST https://gate.reviopay.com/api/v1/purchases/{id}/refund/
{ "amount": 50000 }Omit amount for a full refund. Partial refunds may be repeated until the original total is exhausted. Check refund_availability and refundable_amount on the purchase first — not every payment method supports partial refunds.
A refund cannot be voided. Once submitted it runs to completion; there is no reversal. See Advanced refund management for the full state model.
Cancelling a purchase
POST https://gate.reviopay.com/api/v1/purchases/{id}/cancel/Cancels a purchase that has not been paid. Use /release/ for an authorisation on hold, and /refund/ once a payment has completed.
Webhooks
Webhooks are the authoritative record of what happened. Register once and treat the callback as the trigger for fulfilment.
POST https://gate.reviopay.com/api/v1/webhooks/
{
"title": "Order events",
"callback": "https://www.example.com/webhooks/precium",
"events": ["purchase.paid", "purchase.payment_failure", "payment.charged_back"]
}Pass all_events: true instead of events to subscribe to everything. Test and live webhooks are separate: a test webhook never receives events from live purchases.
Available events
| Group | Events |
|---|---|
| Purchase lifecycle | purchase.created, purchase.viewed, purchase.paid, purchase.payment_failure, purchase.cancelled |
| Authorisation | purchase.hold, purchase.preauthorized, purchase.captured, purchase.capture_failure, purchase.released, purchase.release_failure |
| In flight | purchase.pending_charge, purchase.pending_capture, purchase.pending_release, purchase.pending_refund, purchase.pending_execute |
| Refunds and disputes | payment.refunded, purchase.refund_failure, payment.charged_back, payment.chargeback_reversed |
| Settlement | purchase.settled |
| Tokens | purchase.recurring_token_deleted, purchase.pending_recurring_token_delete |
Further events exist for payouts and subscription billing; those products are documented separately and are not part of the S2S card flow.
Verifying a callback
Payloads are signed with asymmetric public-key cryptography. Each delivery carries an X-Signature header: a base64-encoded RSA PKCS#1 v1.5 signature of the SHA-256 digest of the raw request body.
| Callback | Public key from |
|---|---|
| Webhook subscription | public_key on the webhook object |
Per-purchase success_callback | GET /public_key/ |
The key is a PEM-encoded RSA public key. Verify against the raw body bytes, before parsing or re-serialising — reformatting the JSON invalidates the signature. Precium is not responsible for losses arising from unverified payloads.
Delivery and retries
- Only a 2xx counts as delivered. Failures retry up to 8 further times at exponentially increasing intervals.
- Nothing is attempted more than 36 hours after the triggering event.
- Duplicates are possible even after a 200, if the confirmation is lost. Make handlers idempotent, keyed on the event and object id.
- Delivery is ordered per object: no
purchase.paidfor a purchase until all itspurchase.createdcallbacks have been delivered.
Inspecting delivery history
GET https://gate.reviopay.com/api/v1/webhooks/deliveries/?id={object_id}&source_type=PurchaseReturns every attempt for that object — successes, failures and retries. Both parameters are required. This is the first place to look when a callback appears not to have arrived.
Duplicate protection
There is no idempotency key. Duplicates are prevented by state:
- A purchase may be paid only once; repeat attempts are rejected, typically with
409 Conflict - Client email addresses are unique
- Any number of purchases may exist for one client, each independently payable once
So retry against the same purchase id rather than creating a new purchase, and confirm state with GET /purchases/{id}/ before retrying. Creating a fresh purchase on every retry is what produces double charges.
Rate limits and pagination
The default limit is 10 transactions per second, per IP address. Exceeding it returns 429; back off and retry.
Before go-live, send Precium the production IP addresses that will call the API and receive webhooks, together with the throughput you need. Those IPs are allow-listed and capacity is provisioned against the agreed figure. See Going live.
List endpoints are cursor-paginated as results with next and previous. Follow next; do not build offsets.
Settlement
Transactions are authorised and settled the next day. Settlement of funds to your account may follow a different schedule depending on your commercial model — confirm your cycle with your account manager. Reconciliation file formats are in Reconciliation.
Related pages
- Technical reference documentation — the full endpoint and field reference
- Error mapping — every error code with cause and retry guidance
- Issuer response codes — decline codes returned by issuing banks
- Advanced refund management — the complete refund state model
- Test scenarios — the full pre-launch test matrix