Table of contents

Integration guide

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

KeyUsed for
Standard API keyEvery API operation: clients, purchases, charge, capture, release, refund, cancel, webhooks, reporting
S2S API keySubmitting card data to a purchase's direct_post_url

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.

ClassCardholder3D SecureCVCLiability
CITPresentRequiredRequiredShifts to issuer on successful authentication
CIT → MITPresent for the first paymentRequired on the first paymentRequired on the first paymentShifts on the authenticated first payment
MITNot presentNot presentedNot used — may be stubbedRests 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.

StatusMeaning
createdCreated, awaiting payment
sent / viewedInvoice sent / checkout opened
pending_chargeAuthorisation in flight
holdAuthorised, funds reserved, awaiting capture or release
preauthorizedCard verified with no financial transaction
pending_capture / pending_release / pending_refundOperation accepted, acquirer still processing
paidCaptured and complete
releasedHold released without capture
refundedRefunded in full
cancelledCancelled before payment
chargebackDisputed by the cardholder
expired / overduePayment window elapsed / past its due date
errorFailed — 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 /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 the same fields as JSON to {direct_post_url}?s2s=true using your S2S key. That is the full S2S flow and requires SAQ-D.

Flow 3 — Preauthorisation (card verification)

Verify a card and store it without moving money. Set skip_capture: trueand a purchase total of zero.

POST /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.

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 /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 /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 /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 /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
  }
}
FieldNotes
is_recurringBoolean. Marks the transaction merchant-initiated
previous_network_transaction_idFrom the original authenticated transaction. Ask your account manager if you do not hold it
original_amount_centsInteger, 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.

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.

"payment_method_details": {
  "card": {
    "is_external_3DS": true,
    "authentication_transaction_id": "your-mpi-transaction-id",
    "cavv": "base64-cardholder-authentication-value",
    "xid":  "3ds1-transaction-identifier",
    "eci_raw": "05"
  }
}
FieldRequiredNotes
is_external_3DSYesMust be true
authentication_transaction_idYesYour MPI's identifier for the authentication
cavvYesCardholder Authentication Verification Value
xid3DS1 onlyTransaction identifier
eci_rawYesElectronic Commerce Indicator — see below

ECI values

ECIMeaningLiability shift
05Fully authenticated (Visa)Yes
02Fully authenticated (Mastercard)Yes
06Authentication attempted (Visa)Conditional
01Authentication attempted (Mastercard)Conditional
07Not authenticated (Visa)No
00Not 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 tokens from Visa Token Service or Mastercard Digital Enablement Service, present the token and its cryptogram instead of a PAN.

"payment_method_details": {
  "card": {
    "network_token": "your-network-token",
    "network_token_cryptogram": "dynamic-cryptogram",
    "token_requestor_id": "your-token-requestor-id",
    "is_recurring": true
  }
}

All three token fields are required. Network tokens generally improve authorisation rates and remove the need to handle card expiry updates.

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.

Rather than hard-coding a list, ask the API what a brand actually has enabled:

GET /payment_methods/?brand_id={brand_id}&currency=ZAR

Send 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 /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 /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 /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

GroupEvents
Purchase lifecyclepurchase.created, purchase.viewed, purchase.paid, purchase.payment_failure, purchase.cancelled
Authorisationpurchase.hold, purchase.preauthorized, purchase.captured, purchase.capture_failure, purchase.released, purchase.release_failure
In flightpurchase.pending_charge, purchase.pending_capture, purchase.pending_release, purchase.pending_refund, purchase.pending_execute
Refunds and disputespayment.refunded, purchase.refund_failure, payment.charged_back, payment.chargeback_reversed
Settlementpurchase.settled
Tokenspurchase.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.

CallbackPublic key from
Webhook subscriptionpublic_key on the webhook object
Per-purchase success_callbackGET /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.paid for a purchase until all its purchase.created callbacks have been delivered.

Inspecting delivery history

GET /webhooks/deliveries/?id={object_id}&source_type=Purchase

Returns 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

100 requests per minute, bursting to 200. Responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; back off on 429.

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