What this covers
The shortest path to a successful test transaction: create a client, create a purchase, submit card data, complete 3D Secure, confirm the result. Six requests.
Once this works, the Integration guide covers every flow in depth — preauthorisation, delayed capture, external MPI, network tokens, merchant-initiated charges and the full webhook event set.
Before you start
| You need | Where from |
|---|---|
| Bearer token | Developers section of your dashboard |
| S2S token | Issued by Precium to qualifying merchants. Not on the dashboard |
| Brand ID | Your dashboard. May require approval before live use |
| An HTTPS endpoint | Yours, for the 3D Secure return and for webhooks |
Use your test keys throughout. Test and production share one base URL and are separated only by the key you send, so there is no URL to get wrong — and no URL-level protection if you send the wrong key.
https://gate.reviopay.com/api/v1/Step 0 — Check your credentials
Confirm the key works before building anything:
curl -X GET https://gate.reviopay.com/api/v1/clients/ \
-H "Authorization: Bearer $PRECIUM_API_KEY"An empty result set is success. A 401 means the key is wrong or malformed; check for a stray newline or a missing Bearer prefix.
Step 1 — Create a client
A client is the customer record that purchases attach to. Only email is required, and it must be unique.
curl -X POST https://gate.reviopay.com/api/v1/clients/ \
-H "Authorization: Bearer $PRECIUM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "buyer@example.com",
"full_name": "Test Buyer",
"phone": "+27821234567"
}'Keep the returned id — that is your client_id.
You can skip this step entirely by passing an inline client object when creating the purchase. Precium will find or create the client from the email. Creating the client explicitly is clearer when you already hold customer records.
Step 2 — Create a purchase
Three fields are required: client_id (or an inline client), purchase, and brand_id. All amounts are integers in cents — 149900 is R1,499.00.
curl -X POST https://gate.reviopay.com/api/v1/purchases/ \
-H "Authorization: Bearer $PRECIUM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_id": "00000000-0000-4000-8000-000000000001",
"brand_id": "00000000-0000-4000-8000-000000000002",
"purchase": {
"currency": "ZAR",
"language": "en",
"products": [
{ "name": "Order 1042", "price": 149900 }
]
},
"payment_method_whitelist": ["visa", "mastercard"],
"reference": "ORDER-1042",
"force_recurring": false,
"skip_capture": false,
"success_redirect": "https://www.example.com/success",
"failure_redirect": "https://www.example.com/failure",
"cancel_redirect": "https://www.example.com/cancel"
}'From the response, keep:
| Field | Use |
|---|---|
id | The purchase id. Every later call uses it |
direct_post_url | Where you submit card data in step 3 |
checkout_url | Precium-hosted page, if you would rather not handle card data at all |
status | created at this point |
If direct_post_url is null, it is because success_redirect and failure_redirect were not both supplied. Both are required for the direct post flow.
Two flags to be deliberate about
| Flag | Effect |
|---|---|
skip_capture: false | Authorise and capture in one step. This is what you want for a simple sale |
skip_capture: true | Authorise only. Funds are held and you must later /capture/ or /release/. With a purchase total of 0 this becomes preauthorisation — card verification with no financial transaction |
force_recurring: true | Store the card. The purchase id then becomes the token for future merchant-initiated charges |
Step 3 — Submit the card data
Post the card fields to the direct_post_url from step 2, with ?s2s=true. This request uses your S2S token, not the bearer token.
curl -X POST "$DIRECT_POST_URL?s2s=true" \
-H "Authorization: Bearer $PRECIUM_S2S_KEY" \
-H "Content-Type: application/json" \
-d '{
"cardholder_name": "Test Buyer",
"card_number": "4111111111111111",
"expires": "12/29",
"cvc": "123",
"remember_card": "off",
"remote_ip": "196.0.0.1",
"user_agent": "Mozilla/5.0",
"accept_header": "text/html",
"language": "en-US",
"java_enabled": false,
"javascript_enabled": true,
"color_depth": 24,
"utc_offset": 0,
"screen_width": 1920,
"screen_height": 1080
}'Field rules
| Field | Format |
|---|---|
cardholder_name | Latin letters, plus space, apostrophe, dot and dash. Max 30 characters |
card_number | Digits only, no spaces. Max 19 |
expires | MM/YY exactly — two digits, slash, two digits |
cvc | 3 or 4 digits |
remember_card | on to store the card, off otherwise |
Validate these yourself before submitting. A format error is treated as a payment failure, not as a validation response, so a malformed expiry date looks like a decline.
The browser fields from remote_ip down are the 3D Secure device fingerprint. Send them for customer-initiated payments; they materially affect whether the issuer challenges the cardholder.
Step 4 — Complete 3D Secure
If the card is enrolled, the response carries 3D Secure parameters. Post the customer’s browser to the issuer, then return them to the callback.
<form id='tds' method='POST' action='{{URL}}'>
<input type='hidden' name='MD' value='{{md}}'>
<input type='hidden' name='PaReq' value='{{PaReq}}'>
<input type='hidden' name='TermUrl' value='{{callback_url}}'>
<noscript><button type='submit'>Continue to your bank</button></noscript>
</form>
<script>document.getElementById('tds').submit();</script>Although MD, PaReq and PaRes are 3DS1 parameter names, 3D Secure 2 is fully supported — Precium performs the 3DS2 verification and challenge behind this simpler redirect contract. You do not implement 3DS2 yourself.
If the card is not enrolled, authorisation completes synchronously and there is nothing to redirect.
Step 5 — Confirm the outcome
Read the purchase back:
curl -X GET https://gate.reviopay.com/api/v1/purchases/$PURCHASE_ID/ \
-H "Authorization: Bearer $PRECIUM_API_KEY"| Status | Meaning |
|---|---|
paid | Complete. Fulfil the order |
hold | Authorised only. Capture or release it |
preauthorized | Card verified, no money moved |
pending_charge | In flight. Wait for the webhook |
error | Failed. Read transaction_data.attempts[], newest first, for the error code |
Do not treat the browser redirect as confirmation. A customer who closes the tab still completes a payment, and you will never see the redirect. Webhooks are the authoritative source.
Step 6 — Receive the result by webhook
Register an endpoint once:
curl -X POST https://gate.reviopay.com/api/v1/webhooks/ \
-H "Authorization: Bearer $PRECIUM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Order events",
"callback": "https://www.example.com/webhooks/precium",
"events": ["purchase.paid", "purchase.payment_failure"]
}'Three things your handler must do:
- Verify the signature. Every delivery carries an
X-Signatureheader — a base64-encoded RSA PKCS#1 v1.5 signature of the SHA-256 digest of the raw request body. Get the key frompublic_keyon the webhook object. Verify the raw bytes, before parsing. - Tolerate duplicates. The same event can arrive twice even after you return 200, if the confirmation is lost in transit.
- Return 2xx quickly and process asynchronously. Failures are retried up to 8 further times, and nothing is attempted more than 36 hours after the event.
Test cards
Card numbers for the sandbox are listed in Test scenarios, alongside the expected outcome for each. Use 123 as the CVC for a successful verification.
The examples above use 4111111111111111, the standard Visa test number. Confirm the current set against Test scenarios before building an automated test suite.
When something fails
| Symptom | Most likely cause |
|---|---|
401 Unauthorized | Wrong key, or the standard key used where the S2S key is required |
400 on purchase creation | Missing brand_id, or currency and products placed at the top level instead of inside purchase |
direct_post_url is null | success_redirect and failure_redirect not both supplied |
409 Conflict | The purchase has already been paid. A purchase may be paid only once |
| Looks like a decline, but the card is fine | A card field failed format validation — check expires is MM/YY |
Full error codes, retry guidance and issuer response codes are in Error mapping and Issuer response codes.
Next
- Integration guide — every flow in detail, including preauthorisation, delayed capture, external MPI, network tokens and merchant-initiated charges
- Test scenarios — the full test matrix to work through before going live
- Code examples — the same flow in Python, Node.js, PHP, Ruby, Java, C# and Go