```
# Error mapping (/docs/server-to-server/error-mapping)
## Introduction [#introduction]
This document provides a comprehensive reference for all error codes returned by the Precium S2S API. Understanding these codes enables proper error handling, appropriate customer messaging, and informed decisions about transaction retries.
## Error Response Structure [#error-response-structure]
When a transaction fails, the API returns an error object with the following structure:
```json
{
"error": {
"code": "insufficient_funds",
"description": "Insufficient funds",
"type": "Insufficient funds",
"friendly_header": "Insufficient funds",
"friendly_message": "The transaction failed due to insufficient funds. Fund your account and try again later."
}
}
```
## Error Categories [#error-categories]
Precium errors are categorised by type to help you implement appropriate handling logic:
| Category | Description | Typical Action |
|---|---|---|
| **Authentication failure** | 3DS authentication did not complete successfully | Allow customer retry with same or different card |
| **Charge decline** | Transaction was declined by issuer or acquirer | Investigate cause, may retry based on specific code |
| **Configuration error** | System configuration issue | Contact Precium support immediately |
| **Fraud flag** | Transaction blocked by fraud systems | Investigate, do not retry without review |
| **Insufficient funds** | Customer lacks available balance | Notify customer, retry later |
| **Invalid request** | Request format or content is invalid | Fix request and retry |
| **Invalid token** | Card token is no longer valid | Re-tokenize customer's card |
| **Network error** | Communication failure between systems | Automatic retry appropriate |
| **Provider error** | Issue with payment provider | Contact support, retry later |
| **Validation error** | Input data failed validation | Correct input and retry |
## MIT Retry Classification [#mit-retry-classification]
For Merchant-Initiated Transactions (MIT), errors are classified as **Soft** or **Hard** declines:
| Classification | Meaning | Action |
|---|---|---|
| **Soft** | Temporary issue, may succeed on retry | Implement retry schedule |
| **Hard** | Permanent issue, will not succeed on retry | Do not retry, request new payment method |
## Error Code Quick Reference [#error-code-quick-reference]
**Priority Levels**
| Priority | Response Time | Description |
|---|---|---|
| **URGENT** | Immediate | System configuration issue requiring immediate attention |
| **HIGH** | \< 4 hours | Significant issue affecting transactions |
| **MEDIUM** | \< 24 hours | Issue requiring investigation |
| **LOW** | Standard | Normal decline or validation error |
## Complete Error Code Catalogue [#complete-error-code-catalogue]
### Authentication Failures [#authentication-failures]
**`3ds_authentication_failed`**
| Attribute | Value |
|---|---|
| **Description** | 3DS authentication failed |
| **Priority** | LOW |
| **MIT Retry** | Not applicable (CIT only) |
| **Likely Cause** | Cardholder, 3DS provider, or issuing bank |
**Possible Causes:**
- Incorrect information entered during the 3DS challenge (OTP, password, biometric)
- The issuer's authentication system was temporarily unavailable
- Fraud prevention systems flagged the transaction as suspicious
**Merchant Action:**
- Allow the customer to retry the payment and authentication process
- Provide clear instructions for completing 3DS authentication
- Suggest using another card if available
- Offer an alternative payment method
**Customer Message:***"There was a problem with the authentication process. Try again - with another card if possible. If this issue persists, contact your bank for help, or use another payment method."*
**`timeout_3ds_enrollment_check`**
| Attribute | Value |
|---|---|
| **Description** | 3DS enrollment check timeout |
| **Priority** | HIGH |
| **MIT Retry** | Not applicable |
| **Likely Cause** | 3DS provider or network issues |
**Possible Causes:**
- The enrollment check was not completed within the timeout period
- Network issues between the provider and the 3DS provider
**Merchant Action:**
- Allow the customer to retry payment and authentication
- Monitor for patterns with specific issuers (BINs) or card types
- Contact Precium support if the issue persists
**Customer Message:***"There was a problem with the authentication process. Try again - with another card if possible."*
**`timeout`**
| Attribute | Value |
|---|---|
| **Description** | Operation timeout |
| **Priority** | LOW |
| **MIT Retry** | Not applicable |
| **Likely Cause** | Customer inaction or network issues |
**Customer Message:***"There was a problem with the authentication process. Try again - with another card if possible."*
### Charge Declines [#charge-declines]
**`do_not_honour`**
| Attribute | Value |
|---|---|
| **Description** | Transaction declined by issuer without specific reason |
| **Priority** | LOW |
| **MIT Retry** | Soft |
| **Likely Cause** | Issuer or cardholder |
**Customer Message:***"The transaction was declined by your bank for an unspecified reason. Try again later - with another card if possible. If this issue persists, contact your bank."*
**`insufficient_funds`**
| Attribute | Value |
|---|---|
| **Description** | Insufficient funds in account |
| **Priority** | LOW |
| **MIT Retry** | Soft |
| **Likely Cause** | Cardholder |
**Customer Message:***"The transaction failed due to insufficient funds. Fund your account and try again later, with another card if possible."*
**`authorization_failed`**
| Attribute | Value |
|---|---|
| **Description** | Payment authorization failed |
| **Priority** | LOW |
| **MIT Retry** | Soft |
| **Likely Cause** | Provider, acquirer, issuer, or Precium |
**Customer Message:***"The transaction failed unexpectedly. Try again later - with another card if possible."*
**`exceeded_account_limit`**
| Attribute | Value |
|---|---|
| **Description** | Account transaction limit exceeded |
| **Priority** | LOW |
| **MIT Retry** | Soft |
| **Likely Cause** | Cardholder or issuer |
**Customer Message:***"The transaction was declined by your bank due to your account limit being exceeded."*
**`exceeds_frequency_limit`**
| Attribute | Value |
|---|---|
| **Description** | Transaction frequency limit exceeded |
| **Priority** | LOW |
| **MIT Retry** | Soft |
| **Likely Cause** | Cardholder or issuer |
**Customer Message:***"The transaction was declined by your bank due to your account frequency limit being exceeded."*
**`exceeds_withdrawal_limit`**
| Attribute | Value |
|---|---|
| **Description** | Withdrawal limit exceeded |
| **Priority** | LOW |
| **MIT Retry** | Soft |
| **Likely Cause** | Cardholder or issuer |
**Customer Message:***"The transaction was declined by your bank due to your account withdrawal limit being exceeded."*
**`antifraud_general`**
| Attribute | Value |
|---|---|
| **Description** | Declined due to fraud detection |
| **Priority** | MEDIUM |
| **MIT Retry** | Hard |
| **Likely Cause** | Fraud detection systems |
**Possible Causes:**
- Incomplete or inaccurate client object data
- Different identifiers used with the same card details
- Access from a restricted country
- Velocity or value thresholds exceeded
- 3DS or CVV checks bypassed
**Customer Message:***"The transaction was blocked due to suspected fraud. Try again later, with another card if possible. If this issue persists, use another payment method or contact us for help."*
**`soft_decline_contact_support`**
| Attribute | Value |
|---|---|
| **Description** | Soft decline requiring manual review |
| **Priority** | HIGH |
| **MIT Retry** | Soft |
| **Likely Cause** | Fraud systems flagged for review |
**Customer Message:***"The transaction was blocked due to suspected fraud. Try again later - with another card if possible."*
**`transaction_not_permitted_to_cardholder`**
| Attribute | Value |
|---|---|
| **Description** | Transaction type not permitted for this card |
| **Priority** | LOW |
| **MIT Retry** | Hard |
| **Likely Cause** | Cardholder or issuer card restrictions |
**Customer Message:***"The transaction failed due to the card not supporting this transaction type. Try again with another card, or contact your bank for help."*
**`transaction_not_supported_or_not_valid_for_card`**
| Attribute | Value |
|---|---|
| **Description** | Transaction type not supported by card |
| **Priority** | LOW |
| **MIT Retry** | Hard |
| **Likely Cause** | Card restrictions |
**Customer Message:***"The transaction failed due to the card not supporting this transaction type. Try again with another card."*
### Fraud Flags [#fraud-flags]
**`blacklisted_tx`**
| Attribute | Value |
|---|---|
| **Description** | Transaction blocked - general blacklist |
| **Priority** | MEDIUM |
| **MIT Retry** | Hard |
| **Likely Cause** | Fraud systems |
**Customer Message:***"The transaction was blocked due to suspected fraud. Try again later, with another card if possible."*
**`blacklisted_tx_issuing_country`**
| Attribute | Value |
|---|---|
| **Description** | Card issued in blacklisted country |
| **Priority** | MEDIUM |
| **MIT Retry** | Hard |
| **Likely Cause** | Geographic restrictions |
**Customer Message:***"The transaction was blocked due to suspected fraud. Try again with another card if possible."*
**`blacklisted_tx_risk_score`**
| Attribute | Value |
|---|---|
| **Description** | Risk score threshold exceeded |
| **Priority** | MEDIUM |
| **MIT Retry** | Hard |
| **Likely Cause** | Combined risk factors |
**Customer Message:***"The transaction was blocked due to suspected fraud. Try again with another card if possible."*
**`suspected_fraud`**
| Attribute | Value |
|---|---|
| **Description** | Suspected fraudulent transaction |
| **Priority** | MEDIUM |
| **MIT Retry** | Hard |
| **Likely Cause** | Fraud detection systems |
**Customer Message:***"The transaction was blocked due to suspected fraud. Try again later - with another card if possible."*
### Invalid Token Errors [#invalid-token-errors]
**`expired_card`**
| Attribute | Value |
|---|---|
| **Description** | Card has expired |
| **Priority** | LOW |
| **MIT Retry** | Hard |
| **Likely Cause** | Cardholder, merchant token management |
**Customer Message:***"The transaction failed due to the card having expired. Try again with another card, or contact your bank for help."*
**`lost_card`**
| Attribute | Value |
|---|---|
| **Description** | Card reported as lost |
| **Priority** | MEDIUM |
| **MIT Retry** | Hard |
| **Likely Cause** | Card reported lost by cardholder |
**Customer Message:***"The transaction failed due to the card being reported as lost. Try again with another card."*
**`stolen_card`**
| Attribute | Value |
|---|---|
| **Description** | Card reported as stolen |
| **Priority** | MEDIUM |
| **MIT Retry** | Hard |
| **Likely Cause** | Card reported stolen |
**Customer Message:***"The transaction failed due to the card being reported as stolen. Try again with another card."*
**`restricted_card`**
| Attribute | Value |
|---|---|
| **Description** | Card restricted from this transaction type |
| **Priority** | LOW |
| **MIT Retry** | Hard |
| **Likely Cause** | Card restrictions by issuer |
**Customer Message:***"The transaction failed due to the card being restricted. Try again with another card."*
**`recurring_token_expired`**
| Attribute | Value |
|---|---|
| **Description** | Stored token has expired |
| **Priority** | HIGH |
| **MIT Retry** | Hard |
| **Likely Cause** | Token lifecycle |
**Customer Message:***"The transaction failed due to the card you saved having expired. Try again with another card."*
### Configuration Errors [#configuration-errors]
**`acquirer_configuration_error`**
| Attribute | Value |
|---|---|
| **Description** | Acquirer system misconfiguration |
| **Priority** | URGENT |
| **MIT Retry** | Soft |
| **Likely Cause** | Precium, merchant, or acquirer configuration |
**Customer Message:***"There was a problem processing payment due to a technical issue. Try again later."*
**`no_matching_terminal`**
| Attribute | Value |
|---|---|
| **Description** | No terminal configured for this transaction |
| **Priority** | URGENT |
| **MIT Retry** | Not retryable |
| **Likely Cause** | Routing or configuration issue |
**Customer Message:***"There was a problem processing payment due to a technical issue. Try again later."*
### Network Errors [#network-errors]
**`acquirer_connection_error`**
| Attribute | Value |
|---|---|
| **Description** | Connection to acquirer failed |
| **Priority** | URGENT |
| **MIT Retry** | Soft |
| **Likely Cause** | Network infrastructure |
**Customer Message:***"There was a problem processing payment due to a technical issue. Try again later."*
**`issuer_not_available`**
| Attribute | Value |
|---|---|
| **Description** | Card issuer system unavailable |
| **Priority** | LOW |
| **MIT Retry** | Soft |
| **Likely Cause** | Issuer or network issues |
**Customer Message:***"There was a problem processing payment due to a technical issue. Try again later."*
**`timeout_acquirer_status_check`**
| Attribute | Value |
|---|---|
| **Description** | Timeout checking payment status |
| **Priority** | HIGH |
| **MIT Retry** | Not specified |
| **Likely Cause** | Network timeout |
**Customer Message:***"There was a problem processing payment due to a technical issue. Please try again later."*
### Provider Errors [#provider-errors]
**`acquirer_error_cs`**
| Attribute | Value |
|---|---|
| **Description** | Internal acquirer error |
| **Priority** | HIGH |
| **MIT Retry** | Soft |
| **Likely Cause** | Provider system error |
**Customer Message:***"There was a problem processing payment due to a technical issue. Try again later."*
**`acquirer_internal_error`**
| Attribute | Value |
|---|---|
| **Description** | Acquirer internal system error |
| **Priority** | HIGH |
| **MIT Retry** | Soft |
| **Likely Cause** | Acquirer system issue |
**Customer Message:***"There was a problem processing payment due to a technical issue. Try again later."*
**`acquirer_routing_error`**
| Attribute | Value |
|---|---|
| **Description** | Acquirer routing error |
| **Priority** | HIGH |
| **MIT Retry** | Soft |
| **Likely Cause** | Routing configuration |
**Customer Message:***"There was a problem processing payment due to a technical issue. Please try again later."*
### Validation Errors [#validation-errors]
**`invalid_amount`**
| Attribute | Value |
|---|---|
| **Description** | Invalid transaction amount |
| **Priority** | LOW |
| **MIT Retry** | Not applicable |
| **Likely Cause** | Merchant input |
**Customer Message:***"There was a problem processing payment due to incomplete or invalid data. Check input data and try again."*
**`invalid_card_data`** / **`invalid_card_number`** / **`invalid_expires`**
| Attribute | Value |
|---|---|
| **Description** | Invalid card data format |
| **Priority** | LOW |
| **MIT Retry** | Not applicable |
Note: Expiry must be in **MM/YY** format.
**Customer Message:***"There was a problem processing payment due to incomplete or invalid data."*
**Cardholder Name / Card Number / CVC / Expiry Validation Errors**
These include: `validation_card_number_invalid`, `validation_card_number_not_provided`, `validation_card_number_too_long`, `validation_cardholder_name_invalid`, `validation_cardholder_name_not_provided`, `validation_cvc_invalid`, `validation_cvc_not_provided`, `validation_expires_invalid`, etc.
- Implement client-side Luhn check validation for card numbers
- Ensure card numbers contain only digits
- CVC should be 3 digits (4 for Amex)
- Use **MM/YY** format for expiry dates
### Invalid Request Errors [#invalid-request-errors]
**`s2s_not_supported`**
| Attribute | Value |
|---|---|
| **Description** | S2S flow not configured |
| **Priority** | MEDIUM |
| **MIT Retry** | Not applicable |
**Customer Message:***"There was a problem processing payment due to a technical issue. Use another payment method."*
**`security_violation`**
| Attribute | Value |
|---|---|
| **Description** | Security policy violation |
| **Priority** | MEDIUM |
| **MIT Retry** | Not applicable |
**Customer Message:***"The transaction was blocked due to suspected fraud. Try again later - with another card if possible."*
**`unknown_payment_method`** / **`transaction_not_permitted_on_terminal`**
| Attribute | Value |
|---|---|
| **Description** | Requested payment method not recognized or not configured |
| **Priority** | MEDIUM |
| **MIT Retry** | Not applicable |
### Pre-Authorization and Capture Errors [#pre-authorization-and-capture-errors]
**`capture_amount_exceeds_authorization`**
| Attribute | Value |
|---|---|
| **Description** | Capture amount exceeds authorized amount |
| **Priority** | LOW |
| **MIT Retry** | Not applicable |
| **Likely Cause** | Merchant input |
**`authorization_expired`**
| Attribute | Value |
|---|---|
| **Description** | Pre-authorization has expired |
| **Priority** | MEDIUM |
| **MIT Retry** | Not applicable |
| **Likely Cause** | Authorization lifecycle |
**Customer Message:***"The payment authorization has expired. Please complete a new payment."*
**`void_not_permitted`** / **`already_captured`** / **`already_voided`** / **`partial_capture_not_supported`**
These errors relate to invalid capture, void, or refund operations. Check transaction status before attempting these operations.
### Refund and Void Errors [#refund-and-void-errors]
**`refund_amount_exceeds_original`** / **`refund_void_not_permitted`** / **`transaction_not_refundable`**
Track total refunded amounts and verify transaction status before attempting refunds or voids.
### Preauthorisation Errors [#preauthorisation-errors]
Preauthorisation is the zero-amount card-verification flow (`skip_capture: true` with a purchase total of 0). Its error codes retain the legacy `zero_auth_` prefix.
**`zero_auth_not_permitted`**
| Attribute | Value |
|---|---|
| **Description** | Preauthorisation not allowed |
| **Priority** | LOW |
| **MIT Retry** | Not applicable |
Contact Precium to enable preauthorisation.
### External 3DS/MPI Errors [#external-3dsmpi-errors]
**`invalid_external_3ds_data`**
| Attribute | Value |
|---|---|
| **Description** | External 3DS authentication data invalid |
| **Priority** | LOW |
| **MIT Retry** | Not applicable |
**Required Fields:**
- `is_external_3DS`: true
- `authentication_transaction_id`
- `cavv`
- `eci_raw`
- `xid` (for 3DS1) or `ds_trans_id` (for 3DS2)
**`external_3ds_not_enabled`**
| Attribute | Value |
|---|---|
| **Description** | External 3DS not configured |
| **Priority** | MEDIUM |
| **MIT Retry** | Not applicable |
Contact Precium to enable external 3DS and ensure you're using a Non-3DS brand.
### Webhook Errors [#webhook-errors]
**`webhook_signature_invalid`** / **`webhook_timestamp_expired`** / **`webhook_endpoint_unreachable`** / **`webhook_timeout`**
Verify the signature against the correct **public key** — `public_key` on the webhook object, or `GET /public_key/` for a per-purchase `success_callback`. Verification is asymmetric RSA PKCS#1 v1.5 over the SHA-256 digest of the *raw* body, so re-serialising the JSON before verifying will fail; there is no shared secret. Also confirm the endpoint is reachable and returns within 5 seconds, processing asynchronously.
### Network Token Errors [#network-token-errors]
**`invalid_network_token`** / **`cryptogram_expired`**
Request fresh tokens and cryptograms from MDES/VTS. Submit transactions promptly after cryptogram generation.
### Other Errors [#other-errors]
**`general_transaction_error`** / **`payment_rejected_other_reason`** / **`exceeded_acquirer_refund_amount`**
Contact Precium support for investigation.
## MIT Retry Decision Tree [#mit-retry-decision-tree]
```mermaid
flowchart TD
A[MIT Transaction Failed] --> B{Check Error Code}
B --> C{MIT Retry Classification}
C -->|Hard| D[Do Not Retry]
C -->|Soft| E{Retry Count?}
C -->|Not Specified| F[Evaluate Manually]
D --> G[Request New Payment Method]
E -->|< 4| H[Schedule Retry]
E -->|>= 4| I[Exhaust Retries]
H --> J{Retry #}
J -->|1| K[Wait 24h]
J -->|2| L[Wait 48h]
J -->|3| M[Wait 72h]
J -->|4| N[Wait 96h]
K --> O[Retry Transaction]
L --> O
M --> O
N --> O
I --> G
F --> P{Error Category}
P -->|Network/Provider| H
P -->|Validation/Config| Q[Fix and Retry Immediately]
P -->|Fraud/Card Invalid| D
```
## Error Handling Best Practices [#error-handling-best-practices]
**1. Implement Proper Error Categorisation**
```python
HARD_DECLINE_CODES = [
'expired_card', 'lost_card', 'stolen_card', 'restricted_card',
'recurring_token_expired', 'antifraud_general', 'blacklisted_tx',
'suspected_fraud', 'transaction_not_permitted_to_cardholder'
]
SOFT_DECLINE_CODES = [
'do_not_honour', 'insufficient_funds', 'authorization_failed',
'exceeded_account_limit', 'exceeds_frequency_limit',
'general_transaction_error', 'acquirer_connection_error',
'issuer_not_available'
]
```
**2. Display Customer-Friendly Messages**
Never expose raw error codes to customers. Use the `friendly_header` and `friendly_message` fields from the error response.
**3. Log Comprehensively**
For troubleshooting, log: timestamp, purchase ID, error code, full error response, and request details (excluding sensitive card data).
**4. Set Up Alerts**
Configure alerts for URGENT priority errors (immediate), HIGH priority errors (within 4 hours), error rate thresholds, and unexpected error codes.
## Support Contact [#support-contact]
For unresolved errors or investigation requests:
- **Email:** support@precium.com
- **Priority Issues:** Reference the error code and purchase ID
- **Include:** Timestamp, purchase_id, error response, and steps to reproduce
# Glossary (/docs/server-to-server/glossary)
## A [#a]
### 3D Secure (3DS) [#3d-secure-3ds]
A security protocol that provides an additional layer of authentication for online card transactions. 3DS 2.0 supports risk-based authentication, allowing frictionless flows for low-risk transactions while challenging high-risk ones.
### 3DS Challenge [#3ds-challenge]
The authentication step where the cardholder must verify their identity, typically through an OTP, biometric, or password sent by the issuing bank.
### Acquirer [#acquirer]
A financial institution that processes card payments on behalf of merchants. The acquirer maintains the merchant relationship and routes transactions to the appropriate card networks.
### ACS (Access Control Server) [#acs-access-control-server]
The issuing bank's server that handles 3DS authentication. It generates and validates authentication requests during the 3DS flow.
### AoC (Attestation of Compliance) [#aoc-attestation-of-compliance]
A formal declaration confirming that a merchant or service provider meets PCI DSS requirements. Required annually for S2S integrations.
### API Key [#api-key]
A unique identifier used to authenticate API requests. Precium uses two types: Standard API Key (for general operations) and S2S API Key (for card data submission).
### Authorization [#authorization]
The process of verifying that a cardholder has sufficient funds or credit and obtaining approval from the issuing bank to proceed with a transaction.
## B [#b]
### BIN (Bank Identification Number) [#bin-bank-identification-number]
The first 6-8 digits of a card number that identify the issuing bank and card type. Used for routing decisions and fraud detection.
### Brand ID [#brand-id]
A unique identifier assigned to each merchant brand within the Precium platform. Used in API calls to route transactions correctly.
### Browser Fingerprint [#browser-fingerprint]
A collection of browser and device attributes (screen size, timezone, language, etc.) used for 3DS risk assessment and fraud detection.
## C [#c]
### Callback URL [#callback-url]
A URL Precium sends to the customer after 3DS authentication. The URL receives authentication results for further processing.
### Capture [#capture]
The process of finalising a pre-authorized transaction to transfer funds. Can be full (entire authorized amount) or partial (less than authorized).
### Card Network [#card-network]
Organisations such as Visa, Mastercard, and American Express that operate the payment infrastructure connecting issuers and acquirers.
### Card-Not-Present (CNP) [#card-not-present-cnp]
A transaction where the physical card is not presented, such as online or phone payments. Requires additional fraud prevention measures.
### Card Token [#card-token]
A secure substitute for a card number that can be stored and reused for future transactions without exposing the actual card data.
### CAVV (Cardholder Authentication Verification Value) [#cavv-cardholder-authentication-verification-value]
A cryptographic value generated during 3DS authentication that proves the cardholder was authenticated. Included in authorization requests for liability shift.
### Chargeback [#chargeback]
A transaction reversal initiated by the cardholder through their issuing bank, typically due to fraud, non-receipt of goods, or disputes.
### CIT (Customer-Initiated Transaction) [#cit-customer-initiated-transaction]
A transaction where the cardholder is actively participating at the time of payment, requiring their authentication.
### Clear PAN [#clear-pan]
The actual card number (Primary Account Number) as opposed to a tokenized representation.
### Cryptogram [#cryptogram]
A dynamic security value generated by a network token service (MDES/VTS) that provides transaction-specific authentication.
### CVC/CVV (Card Verification Code/Value) [#cvccvv-card-verification-codevalue]
The 3 or 4-digit security code printed on a card, used to verify card-not-present transactions.
## D [#d]
### Delayed Capture [#delayed-capture]
Reserving funds on a cardholder's account without capturing them, by sending `skip_capture: true` with a non-zero purchase total. The purchase reaches `hold` and must be resolved with `POST /purchases/{id}/capture/` or `POST /purchases/{id}/release/`. Formerly called "pre-authorisation" or "auth-only"; in this documentation *Preauthorisation* means the zero-amount verification flow instead.
### Direct Post URL [#direct-post-url]
A dynamically generated, single-use URL where card data is securely submitted in the S2S flow. Unique to each purchase.
### DPAN (Device Primary Account Number) [#dpan-device-primary-account-number]
A network token that represents the actual card number. Used in network tokenization (MDES/VTS).
### DS (Directory Server) [#ds-directory-server]
A card network component that routes 3DS messages between the merchant and the issuer's ACS.
### DS Transaction ID [#ds-transaction-id]
A unique identifier assigned by the Directory Server in 3DS 2.x authentication, used to link authentication to the transaction.
## E [#e]
### ECI (Electronic Commerce Indicator) [#eci-electronic-commerce-indicator]
A code returned during 3DS authentication that indicates the level of authentication achieved:
- 05/02: Fully authenticated (Visa/Mastercard)
- 06/01: Attempted authentication
- 07/00: Non-3DS or failed authentication
## F [#f]
### Force Recurring [#force-recurring]
A flag in purchase creation that signals intent to store card credentials for future recurring transactions. Generates a token when enabled.
### Fraud Score [#fraud-score]
A numerical value representing the risk level of a transaction based on various factors including velocity, geography, device fingerprint, etc.
### Frictionless Flow [#frictionless-flow]
A 3DS authentication that completes without cardholder interaction because the risk assessment deemed the transaction low-risk.
## G [#g]
### Gateway [#gateway]
A service that securely transmits transaction data between merchants and payment processors.
## H [#h]
### Hard Decline [#hard-decline]
A permanent decline that should not be retried. Indicates issues like invalid card, stolen card, or closed account.
## I [#i]
### Idempotency [#idempotency]
A property where repeating the same request produces the same result, preventing duplicate transactions or charges.
### Issuer / Issuing Bank [#issuer--issuing-bank]
The financial institution that issued the card to the cardholder and is responsible for authorizing transactions and managing the cardholder's account.
## L [#l]
### Liability Shift [#liability-shift]
Transfer of fraud liability from the merchant to the issuing bank upon successful 3DS authentication.
### Luhn Check [#luhn-check]
A checksum algorithm used to validate card numbers. The last digit of a card number is a check digit calculated using this algorithm.
## M [#m]
### MCC (Merchant Category Code) [#mcc-merchant-category-code]
A four-digit code classifying the type of business a merchant operates. Affects interchange rates and transaction permissions.
### MD (Merchant Data) [#md-merchant-data]
An opaque value passed through the 3DS flow to maintain state between the redirect to the issuer and the callback.
### MDES (Mastercard Digital Enablement Service) [#mdes-mastercard-digital-enablement-service]
Mastercard's network tokenization service that provisions and manages network tokens for secure digital payments.
### MIT (Merchant-Initiated Transaction) [#mit-merchant-initiated-transaction]
A transaction initiated by the merchant when the cardholder is not present, such as a subscription renewal or scheduled payment.
### MPI (Merchant Plug-In) [#mpi-merchant-plug-in]
A software component that handles 3DS authentication on behalf of a merchant. Can be provided by Precium (internal) or a third-party provider (external).
## N [#n]
### Network Token [#network-token]
A card token provisioned by the card network (Visa, Mastercard) that updates automatically when a card is reissued, providing higher authorization rates.
### Network Transaction ID [#network-transaction-id]
A unique identifier assigned by the card network to link related transactions (e.g., original CIT and subsequent MITs).
## O [#o]
### OTP (One-Time Password) [#otp-one-time-password]
A temporary code sent to the cardholder during 3DS authentication, typically via SMS or an authenticator app.
## P [#p]
### PaReq (Payer Authentication Request) [#pareq-payer-authentication-request]
A message sent to the issuer's ACS containing transaction details for 3DS authentication.
### Partial Capture [#partial-capture]
Capturing less than the pre-authorized amount. Used when the final transaction amount is lower than the original authorisation.
### Partial Refund [#partial-refund]
Returning only a portion of the original transaction amount to the customer.
### PCI DSS (Payment Card Industry Data Security Standard) [#pci-dss-payment-card-industry-data-security-standard]
A set of security standards designed to ensure all companies that accept, process, store, or transmit credit card information maintain a secure environment.
### Preauthorisation [#preauthorisation]
Cardholder verification with no financial transaction: `skip_capture: true` with a purchase total of 0. The purchase reaches `preauthorized` and the card is stored. No funds are reserved — for that, see *Delayed Capture*. Error codes for this flow retain the legacy `zero_auth_` prefix.
### Purchase [#purchase]
In Precium's API, a purchase object represents a transaction intent including amount, currency, products, and redirect URLs.
## Q [#q]
### QSA (Qualified Security Assessor) [#qsa-qualified-security-assessor]
An organisation certified by the PCI Security Standards Council to assess and validate compliance with PCI DSS.
## R [#r]
### RoC (Report on Compliance) [#roc-report-on-compliance]
A detailed document created by a QSA documenting an organisation's compliance status with PCI DSS requirements.
### Recurring Payment [#recurring-payment]
A series of transactions charged to a customer's card at regular intervals based on prior authorization.
### Refund [#refund]
The return of funds to a cardholder, either full or partial, that reverses a previous successful charge.
## S [#s]
### S2S (Server-to-Server) [#s2s-server-to-server]
An integration pattern where card data is transmitted directly from the merchant's server to Precium's API, requiring PCI DSS SAQ-D compliance.
### SAQ (Self-Assessment Questionnaire) [#saq-self-assessment-questionnaire]
A PCI DSS compliance validation tool. Different types exist based on how card data is handled:
- **SAQ A**: Card data never touches merchant systems
- **SAQ A-EP**: Card data touches merchant systems via redirect
- **SAQ D**: Merchant stores, processes, or transmits card data (required for S2S)
### Settlement [#settlement]
The process of transferring funds from the cardholder's issuing bank to the merchant's acquiring bank after transaction authorization.
### Soft Decline [#soft-decline]
A temporary decline that may succeed if retried later. Often due to insufficient funds, velocity limits, or temporary system issues.
## T [#t]
### Token / Tokenization [#token--tokenization]
The process of replacing sensitive card data with a non-sensitive substitute (token) that can be safely stored and used for future transactions.
### Transaction ID [#transaction-id]
A unique identifier assigned to each payment transaction for tracking and reference purposes.
## V [#v]
### Velocity Check [#velocity-check]
Fraud prevention rules that limit the number or value of transactions within a time period to detect suspicious patterns.
### Void [#void]
Cancellation of an authorized transaction before settlement occurs. Also used to cancel refunds before settlement.
### VTS (Visa Token Service) [#vts-visa-token-service]
Visa's network tokenization service that provisions and manages network tokens for secure digital payments.
## W [#w]
### Webhook [#webhook]
An HTTP callback that Precium sends to your server when specific events occur (payment completed, refund processed, etc.).
### Webhook Public Key [#webhook-public-key]
The PEM-encoded RSA public key used to verify that a callback genuinely came from Precium. Signing is **asymmetric** — Precium holds the private key and you hold only the public key, so there is no shared secret to store or rotate. Read it from `public_key` on the webhook object, or from `GET /public_key/` for a per-purchase `success_callback`.
### Webhook Signature [#webhook-signature]
A base64-encoded **RSA PKCS#1 v1.5** signature of the SHA-256 digest of the raw callback body, sent in the `X-Signature` header. Verification is asymmetric — there is no shared secret. Obtain the public key from `public_key` on the webhook object, or from `GET /public_key/` for a per-purchase `success_callback`. Verify the raw body bytes before parsing; re-serialising the JSON invalidates the signature.
## X [#x]
### XID (Transaction Identifier) [#xid-transaction-identifier]
A unique identifier used in 3DS 1.x authentication to link the authentication to the transaction.
## Common Abbreviations Quick Reference [#common-abbreviations-quick-reference]
| Abbreviation | Full Term |
|---|---|
| 3DS | 3D Secure |
| ACS | Access Control Server |
| AoC | Attestation of Compliance |
| API | Application Programming Interface |
| BIN | Bank Identification Number |
| CAVV | Cardholder Authentication Verification Value |
| CIT | Customer-Initiated Transaction |
| CNP | Card Not Present |
| CVC/CVV | Card Verification Code/Value |
| DPAN | Device Primary Account Number |
| DS | Directory Server |
| ECI | Electronic Commerce Indicator |
| MCC | Merchant Category Code |
| MD | Merchant Data |
| MDES | Mastercard Digital Enablement Service |
| MIT | Merchant-Initiated Transaction |
| MPI | Merchant Plug-In |
| OTP | One-Time Password |
| PAN | Primary Account Number |
| PaReq | Payer Authentication Request |
| PCI | Payment Card Industry |
| QSA | Qualified Security Assessor |
| RoC | Report on Compliance |
| S2S | Server-to-Server |
| SAQ | Self-Assessment Questionnaire |
| VTS | Visa Token Service |
| XID | Transaction Identifier (3DS1) |
## Currency Codes (ISO 4217) [#currency-codes-iso-4217]
| Code | Currency |
|---|---|
| ZAR | South African Rand |
| USD | United States Dollar |
| EUR | Euro |
| GBP | British Pound Sterling |
| AUD | Australian Dollar |
| CAD | Canadian Dollar |
## HTTP Status Codes [#http-status-codes]
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created (resource successfully created) |
| 400 | Bad Request (invalid input) |
| 401 | Unauthorized (invalid API key) |
| 403 | Forbidden (insufficient permissions) |
| 404 | Not Found (resource doesn't exist) |
| 422 | Unprocessable Entity (validation error) |
| 429 | Too Many Requests (rate limited) |
| 500 | Internal Server Error |
| 502 | Bad Gateway |
| 503 | Service Unavailable |
# Going live (/docs/server-to-server/going-live)
Test and production share one base URL. The environment is selected by the
credentials and the Brand ID you send, not by the host you call, so going live
is a matter of configuration and approval rather than re-pointing your
integration.
## Credentials [#credentials]
Production credentials can be issued for controlled production testing before a
merchant is fully live. A merchant go-live additionally depends on completing
the applicable onboarding and KYC requirements.
The **bearer token** is self-service: generate it in the Developers section of
the dashboard. The **S2S token is not**. It is issued by Precium to qualifying
merchants only, because submitting clear PAN data puts the integration in PCI
DSS SAQ-D scope. Request it well before you need it, in both environments.
Production portal users must use **email addresses different from those on the
test account**. The same address cannot hold both.
## Low-value production testing MID [#low-value-production-testing-mid]
A low-value production testing MID is a production configuration used to
validate end-to-end behaviour against real money, including reconciliation and
settlement.
| | |
|---|---|
| What it is for | Proving the full path works in production, including recon and settlement |
| What it is not | The merchant's live MID. Do not use it for normal processing |
| Descriptor | Fixed, and cannot be changed. This is an acquirer compliance requirement |
| Amounts | Use at least ZAR 1.00. See [amount limits](/docs/server-to-server/integration-guide#amount-limits) |
## Throughput and IP allow-listing [#throughput-and-ip-allow-listing]
The default limit is **10 transactions per second, per IP address**.
Before launch, send Precium:
- the **production IP addresses** that will call the API,
- the **production IP addresses** that will receive webhooks,
- the **transactions per second** you need.
Those addresses are allow-listed and capacity is provisioned against the agreed
figure. Traffic from an address that has not been submitted, or above the agreed
rate, will be rate limited.
## Merchant onboarding inputs [#merchant-onboarding-inputs]
Onboarding requirements depend on the merchant and its risk profile. It starts
with company registration and ownership information, and additional KYC and
beneficial-owner information is required before cross-border settlement, with
enhanced due diligence for higher-risk merchants.
For the acquiring-bank MID application, Precium may also request operational and
risk information:
- average delivery time,
- historic chargeback ratio,
- historic refund ratio,
- forecast transaction volume.
## Pre-integration testing [#pre-integration-testing]
Precium can provide Postman recipes once the payment-feature checklist is
complete. Dashboard access and API credentials are issued when the environment
is provisioned.
## Checklist [#checklist]
- [ ] Use the **S2S token** only for requests carrying clear PAN data, and the **bearer token** for everything else.
- [ ] Store **Brand IDs per merchant processing configuration**, not one per partner. A merchant with two MCCs has two Brand IDs.
- [ ] Implement the **3DS hand-off exactly as returned**. The ACS value is not a plain redirect URL. See [Completing a 3D Secure authentication](/docs/server-to-server/integration-guide#completing-a-3d-secure-authentication).
- [ ] Send amounts **in cents**, and at least **ZAR 1.00** for anything that must settle.
- [ ] Send **real customer and cardholder data** wherever it is available.
- [ ] Verify webhook signatures against the **raw request body**.
- [ ] Treat **webhooks as the state-change mechanism** and `GET /purchases/{id}/` as a current-state lookup.
- [ ] Agree **TPS** and submit **production API and webhook IPs** for allow-listing.
- [ ] Reconcile on **`gateway_transaction_id`**, keeping RRN, authorisation code and amount as secondary data. See [Reconciliation files](/docs/reconciliation).
- [ ] **Release** authorisation holds you do not intend to capture.
- [ ] Confirm the merchant's **card schemes, international-card rules, descriptor and settlement schedule**.
# Integration guide (/docs/server-to-server/integration-guide)
## Scope [#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 [#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 [#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 [#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) [#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.
```bash
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) [#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.
```html
```
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](/docs/server-to-server/integration-quick-start) for a
complete request.
## The Direct Post response [#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 [#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** |
1. Send the customer's browser to `URL` using `Method`, carrying `PaReq` and
`MD` as you received them, plus a `TermUrl` of your own. Use query
parameters for `GET` and body parameters for `POST`.
2. 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.
3. On completion the issuer redirects the browser by **`POST` to your
`TermUrl`**, with `MD` and `PaRes` in the request body.
4. Send `MD` and `PaRes` back to Precium **from your server** to complete the
authorisation. This call takes **no authentication headers**.
5. 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) [#flow-3--preauthorisation-card-verification]
Verify a card and store it without moving money. Set `skip_capture: true`**and** a purchase total of zero.
```bash
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 [#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](/docs/reconciliation).
## Purchase due time [#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) [#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 [#capture]
```bash
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 [#release]
```bash
POST https://gate.reviopay.com/api/v1/purchases/{id}/release/
```
Releases the hold without taking payment.
### Both are asynchronous [#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) [#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 [#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 [#charge-the-stored-card]
Create a new purchase for the new amount, then charge it against the original:
```bash
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 [#list-and-delete-tokens]
```bash
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) [#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.
```bash
"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 [#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](/docs/reference/card) 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) [#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.
```json
"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-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 [#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.
```json
"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 [#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.
```bash
"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:
```bash
GET https://gate.reviopay.com/api/v1/payment_methods/?brand_id={brand_id}¤cy=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 [#refunds]
```bash
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 [#cancelling-a-purchase]
```bash
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]
Webhooks are the authoritative record of what happened. Register once and treat the callback as the trigger for fulfilment.
```bash
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 [#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 [#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 [#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 [#inspecting-delivery-history]
```bash
GET https://gate.reviopay.com/api/v1/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 [#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 [#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](/docs/server-to-server/going-live).
List endpoints are cursor-paginated as `results` with `next` and `previous`. Follow `next`; do not build offsets.
## Settlement [#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 [#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
# Integration quick start (/docs/server-to-server/integration-quick-start)
## What this covers [#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 [#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.
```text
https://gate.reviopay.com/api/v1/
```
## Step 0 — Check your credentials [step] [#step-0--check-your-credentials-step]
Confirm the key works before building anything:
```bash
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 [step] [#step-1--create-a-client-step]
A client is the customer record that purchases attach to. Only `email` is required, and it must be unique.
```bash
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 [step] [#step-2--create-a-purchase-step]
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.
```bash
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 [step] [#two-flags-to-be-deliberate-about-step]
| 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 [step] [#step-3--submit-the-card-data-step]
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.
```bash
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 [step] [#field-rules-step]
| 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 [step] [#step-4--complete-3d-secure-step]
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.
```html
```
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 [step] [#step-5--confirm-the-outcome-step]
Read the purchase back:
```bash
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 [step] [#step-6--receive-the-result-by-webhook-step]
Register an endpoint once:
```bash
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:
1. **Verify the signature.** Every 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. Get the key from `public_key` on the webhook object. Verify the *raw bytes*, before parsing.
2. **Tolerate duplicates.** The same event can arrive twice even after you return 200, if the confirmation is lost in transit.
3. **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 [#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 [#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 [#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
# Introduction (/docs/server-to-server/introduction)
The Precium **Server-to-Server (S2S)** API gives you direct control of the full card payment flow. You handle card data on your own infrastructure and call Precium for authentication, authorisation, capture, refunds and reporting.
This is the most flexible of the Precium integrations and the only one that requires full PCI DSS scope. If you do not need card data on your servers, one of the lower-scope options below will get you live faster.
## Choosing an integration [#choosing-an-integration]
All four options run on the same API and the same credentials. They differ only in where card data is entered, and therefore in your PCI DSS obligation.
| Integration | PCI scope | Card data entered | Best for |
|---|---|---|---|
| **Hosted Checkout** | SAQ-A | On a Precium-hosted page | Fastest route live, minimal compliance burden |
| **Payment Links** | SAQ-A | On a Precium-hosted page | Invoicing, email and manual billing |
| **Direct Post** | SAQ A-EP | In your form, posted straight to Precium | Branded checkout without card data touching your servers |
| **Server-to-Server** | SAQ-D | On your server | PSPs and platforms needing full control of the flow |
Direct Post is worth a look before committing to S2S: you keep a fully branded checkout, but because card data posts from the customer's browser directly to Precium, your scope is raised only to SAQ A-EP rather than SAQ-D. See the **Payment Orchestration** documentation for both hosted and Direct Post flows.
## Who this is for [#who-this-is-for]
- **Payment service providers and payment facilitators** serving their own merchant networks
- **Enterprise merchants** with an existing PCI-compliant cardholder data environment
- **Platforms and marketplaces** embedding payments where the checkout must be fully owned
## Prerequisites [#prerequisites]
| Requirement | Detail |
|---|---|
| **PCI DSS** | Level 1 validation for your cardholder data environment, evidenced by a current Attestation of Compliance. Your Attestation must be on file with Precium before live keys are issued. |
| **Credentials** | A **bearer token** and a **Brand ID** from the Developers section of your dashboard, plus an **S2S token** issued separately by Precium to qualifying merchants. |
| **HTTPS endpoints** | TLS 1.2 or higher, for 3D Secure redirects and webhook delivery. |
| **Webhook handling** | An endpoint that verifies signatures and tolerates duplicate delivery. See *Webhooks* below. |
Your brand may need to be approved by [support@precium.com](mailto:support@precium.com) before it can be used for live transactions.
## Base URL and environments [#base-url-and-environments]
Every request goes to:
```text
https://gate.reviopay.com/api/v1/
```
Endpoint paths in this documentation are relative to that base. For example, `/purchases/` means `https://gate.reviopay.com/api/v1/purchases/`.
**Test and production share this base URL.** The environment is determined entirely by which API key you send — there is no separate sandbox host. Two consequences worth designing around:
- A key mixed up in configuration will send **live** card traffic through what you believe is a test integration. There is no URL-level safeguard, so keep test and live keys in separate secret stores and never fall back from one to the other.
- Test and live data are fully separate. A test webhook never receives events caused by live purchases, and vice versa.
## Authentication [#authentication]
Precium uses bearer token authentication. Send your API key in the `Authorization` header of every request:
```text
Authorization: Bearer YOUR_API_KEY
```
| Key | Used for |
|---|---|
| **Standard API key** | All API operations: creating clients and purchases, capture, refund, cancel, webhooks and reporting |
| **S2S API key** | Submitting card data to the `direct_post_url` returned on a purchase |
Creating a purchase requires both your API key and your **Brand ID**. Never expose either in client-side code, mobile applications or logs.
## Core endpoints [#core-endpoints]
| Method | Path | Purpose |
|---|---|---|
| POST | `/clients/` | Create a customer record |
| POST | `/purchases/` | Create a transaction |
| GET | `/purchases/{id}/` | Retrieve a transaction and its attempt history |
| POST | `/purchases/{id}/charge/` | Charge a stored token |
| POST | `/purchases/{id}/capture/` | Capture funds held by an authorisation |
| POST | `/purchases/{id}/release/` | Release held funds without capturing |
| POST | `/purchases/{id}/refund/` | Refund in full or in part |
| POST | `/purchases/{id}/cancel/` | Cancel a purchase |
| POST | `/purchases/{id}/delete_recurring_token/` | Delete a stored card token |
| GET | `/clients/{id}/recurring_tokens/` | List a customer's stored tokens |
| GET | `/payment_methods/` | List methods enabled for a brand and currency |
| GET | `/public_key/` | Retrieve the key for verifying callback signatures |
## How transactions are classified [#how-transactions-are-classified]
Precium classifies every transaction by who initiated it. This determines whether 3D Secure is required, whether a CVC is needed, and where chargeback liability sits — so it is the first thing to establish when choosing a flow.
| Class | Meaning | 3D Secure | CVC |
|---|---|---|---|
| **CIT** | Customer-initiated. The cardholder is present and acting. | Required | Required |
| **CIT → MIT** | A customer-initiated payment that stores a token for later merchant-initiated use. | Required on the first payment | Required on the first payment |
| **MIT** | Merchant-initiated against a stored token. The cardholder is not present. | Not presented | Not used — may be stubbed |
In South Africa the first charge on a card must always be authenticated with 3D Secure. Subsequent merchant-initiated charges against the resulting token do not present a challenge.
### Supported flows [#supported-flows]
- **Regular redirect** — create a purchase, send the customer to the returned `checkout_url`
- **Direct Post** — your form, card data posted straight to Precium (SAQ A-EP)
- **Preauthorisation** — verify a card and store it without moving money
- **Delayed capture** — hold funds now, capture or release later
- **Tokenised instant capture** — charge a stored token immediately
- **Tokenised delayed capture** — hold against a stored token, capture later
- **External MPI** — bring your own 3D Secure provider
- **External network tokens** — present your own VTS or MDES token and cryptogram
- **Non-3DS** — for approved merchants only; see below
## Preauthorisation and delayed capture [#preauthorisation-and-delayed-capture]
These are distinct operations and the difference matters. Both use `skip_capture`, and the amount decides which one you get.
| Operation | Request | Resulting status | Effect |
|---|---|---|---|
| **Preauthorisation** | `skip_capture: true` with a purchase total of **0** | `preauthorized` | Cardholder verification only. No financial transaction. The card is stored. |
| **Delayed capture** | `skip_capture: true` with a **non-zero** total | `hold` | Funds are reserved. Complete with `/capture/` or release with `/release/`. |
A purchase in `hold` must be resolved. Capture takes an optional `amount` to capture less than was authorised; anything not captured is released. Both operations are asynchronous: if the acquirer is slow you receive HTTP 200 with a status of `pending_capture` or `pending_release`, and the outcome arrives by webhook.
## Refunds [#refunds]
Refund a settled purchase in full, or in part by sending an `amount`. Partial refunds may be issued repeatedly until the original total is exhausted — check `refund_availability` and `refundable_amount` on the purchase before refunding, since not every payment method supports partial refunds.
**A refund cannot be voided.** Once submitted it runs to completion; there is no reversal operation.
## Non-3DS processing [#non-3ds-processing]
Processing without a 3D Secure challenge is available to approved merchants. It is not prohibited, but it changes your risk and cost position:
- Transactions attract a **higher interchange fee** and are treated as higher risk
- You do not receive the liability shift that authentication provides, so chargeback exposure sits with you
- Issuer decline rates are typically higher
Approval is per brand. Contact your account manager before building against this flow.
## Your first request [#your-first-request]
Create a purchase. `client` or `client_id`, `purchase` and `brand_id` are all required. All amounts are integers in **cents**.
```bash
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",
"products": [
{ "name": "Order 1042", "price": 149900 }
]
},
"reference": "ORDER-1042",
"success_redirect": "https://www.example.com/success",
"failure_redirect": "https://www.example.com/failure"
}'
```
The response contains the purchase `id`, a `checkout_url` for hosted checkout, and a `direct_post_url` for submitting card data yourself. Note that `direct_post_url` is returned only when both `success_redirect` and `failure_redirect` are supplied.
## Integration flow [#integration-flow]
```mermaid
sequenceDiagram
participant M as Your server
participant P as Precium
participant I as Issuer
M->>P: POST /purchases/
P-->>M: purchase id + direct_post_url
M->>P: POST direct_post_url?s2s=true (card data)
P-->>M: 3D Secure parameters
M->>I: Redirect cardholder to authenticate
I-->>P: Authentication result
P-->>M: Webhook: purchase.paid
```
## 3D Secure [#3d-secure]
**3D Secure 2 is fully supported.** The S2S flow exchanges `MD`, `PaReq` and `PaRes`, which are 3DS1 parameter names, but these operate as a proxy ACS wrapper: Precium performs full 3DS2 verification and challenge on your behalf while presenting the simpler redirect contract to your integration. You do not need to implement 3DS2 device fingerprinting or the challenge flow yourself.
If you already run your own Merchant Plug-In, use the External MPI flow and pass your own authentication results instead.
## Webhooks [#webhooks]
Webhooks are the authoritative source of transaction outcomes. Do not rely on browser redirects — a customer who closes the tab still completes a payment.
### Verifying a callback [#verifying-a-callback]
Payloads are signed with **asymmetric public-key cryptography**. Every delivery carries an `X-Signature` header containing a base64-encoded **RSA PKCS#1 v1.5** signature of the **SHA-256 digest of the raw request body**.
| Callback type | Where to get the public key |
|---|---|
| Webhook subscription | `public_key` on the corresponding webhook object |
| Per-purchase success callback | `GET /public_key/` |
Verify against the **raw body bytes**, before any JSON parsing or re-serialisation — reformatting the payload invalidates the signature. Precium is not responsible for losses arising from unverified payloads.
### Delivery behaviour [#delivery-behaviour]
- A delivery is successful only on a 2xx response. Failures are retried up to **8 further times** at exponentially increasing intervals.
- No delivery is attempted more than **36 hours** after the triggering event.
- **Duplicates are possible even after you return 200**, if the confirmation is lost in transit. Handlers must be idempotent.
- Deliveries are sequential per object: no `purchase.paid` callback is sent for a purchase until every `purchase.created` callback for it has been delivered.
Twenty-nine event types are available, covering the purchase lifecycle, payments, refunds, chargebacks, payouts and settlement. Subscribe only to what you act on. See *Integration guide* for the full list.
## Duplicate protection [#duplicate-protection]
There is no idempotency key. Precium prevents duplicates through transaction state instead:
- Every purchase has a unique identifier and **may be paid only once**
- Repeat attempts against an already-paid purchase are rejected, typically with `409 Conflict`
- You may create any number of purchases for a client, but each is independently payable once
Practically: create one purchase per order attempt and retry against *that same purchase id* rather than creating another. Confirm state with `GET /purchases/{id}/` before retrying anything.
## Rate limits and pagination [#rate-limits-and-pagination]
Requests are limited to **100 per minute**, with short bursts to 200. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; back off when you see `429`.
List endpoints are cursor-paginated, returning `results` with `next` and `previous` URLs. Follow `next` rather than constructing offsets.
## Settlement [#settlement]
Transactions are authorised and settled the **next day**. Settlement of funds to your account may be scheduled differently depending on your commercial model, so confirm your settlement cycle with your account manager rather than inferring it from transaction timing.
## Going live [#going-live]
1. Complete your integration against test credentials
2. Work through the scenarios in *Test scenarios*, including declines, expiries and 3D Secure failures
3. Confirm your webhook endpoint verifies signatures and handles duplicate delivery
4. Place your PCI Attestation of Compliance on file with Precium
5. Request live credentials and brand approval from your account manager
## Support [#support]
Technical questions, credential requests and brand approvals: [support@precium.com](mailto:support@precium.com).
For the full endpoint reference, including every request field and response code, see *Technical reference documentation*.
# Issuer response codes (/docs/server-to-server/issuer-response-codes)
## Issuer Response Codes (F39) [#issuer-response-codes-f39]
The ISO 8583 Field 39 (F39) response code is the authoritative indicator of why a card transaction was approved or declined. This code comes directly from the card issuer and provides the most accurate reason for transaction outcomes, without intermediary error mapping that might obscure the actual reason.
## Finding the Response Code [#finding-the-response-code]
The issuer response code is located in the `transaction_data.attempts[].extra.response_code` field of the purchase response.
### Example Response [#example-response]
```json
{
"client": {
"email": "sarah.johnson@example.com",
"phone": "+27821234567",
"full_name": "Sarah Johnson"
},
"purchase": {
"currency": "ZAR",
"products": [
{
"name": "Monthly Subscription",
"price": 29900,
"quantity": "1.0000"
}
],
"total": 29900
},
"transaction_data": {
"payment_method": "",
"flow": "server_to_server",
"extra": {
"card_type": "debit",
"card_brand": "visa",
"card_issuer": "first national bank",
"card_issuer_country": "ZA"
},
"attempts": [
{
"type": "execute",
"successful": false,
"payment_method": "visa",
"flow": "server_to_server",
"extra": {
"RRN": "502847291038",
"card_type": "debit",
"card_brand": "visa",
"descriptor": "ACME Corp",
"masked_pan": "411111******1111",
"card_issuer": "first national bank",
"expiry_year": 27,
"expiry_month": 9,
"card_category": "PERSONAL",
"response_code": "51",
"cardholder_name": "SARAH JOHNSON",
"commerce_indicator": "internet",
"card_issuer_country": "ZA",
"network_transaction_id": "0115XYZABC123"
},
"country": "ZA",
"client_ip": "192.168.1.100",
"processing_time": 1768397734,
"processing_status": "{\"class\":\"external\",\"code\":\"INSUFFICIENT_FUND\",\"message\":\"Decline - Insufficient funds in the account.\"}",
"error": {
"code": "insufficient_funds",
"message": "Insufficient funds"
}
}
]
},
"status": "error",
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```
### Key Fields [#key-fields]
| Field Path | Description |
|---|---|
| `transaction_data.attempts[].extra.response_code` | The F39 issuer response code |
| `transaction_data.attempts[].extra.masked_pan` | Masked card number |
| `transaction_data.attempts[].extra.card_issuer` | Issuing bank name |
| `transaction_data.attempts[].processing_status` | Detailed processing status with mapped error |
| `transaction_data.attempts[].error` | Simplified error object |
## Soft Decline Codes [#soft-decline-codes]
Soft declines are temporary failures that may succeed if retried.
| Response Code | Definition |
|---|---|
| `01` | Invalid Card length |
| `01` | VSDC STIP Default Response Code |
| `01` | CVV2 STIP Default Response Code |
| `04` | CVV2 STIP Default Response Code |
| `05` | Missing Expiration Date |
| `05` | VSDC STIP Default Response Code |
| `05` | Decline Key Entered transactions in STIP |
| `05` | RTD Decline in STIP |
| `05` | Decline in STIP transactions acquired in a Risky Country |
| `05` | Issuer Default STIP Response Code (Transaction Type and/or MCG) |
| `05` | OCT Rules Engine Decline |
| `05` | CAM not performed due to missing data |
| `05` | CAM not eligible for CHIP txn |
| `05` | AA score greater than AA STIP |
| `05` | TTL expired, auth/fin txns only |
| `10` | Partial Approval |
| `14` | No Issuer for Account Number |
| `21` | PCAS Reversal cannot find Original Activity |
| `54` | Invalid PAN length |
| `54` | Expired Card |
| `54` | VSDC STIP Default Response Code |
| `55` | VSDC STIP Default Response Code |
| `55` | International transaction — PIN Not Present |
| `55` | Domestic PIN Not Present |
| `57` | Invalid Check Digit |
| `57` | International ATM transaction not permitted for this Service Code |
| `57` | Account Range Blocked |
| `61` | Activity Amount Limit Exceeded |
| `62` | Restricted Country |
| `65` | Activity Count Limit Exceeded |
| `75` | VSDC STIP Default Response Code |
| `82` | Incorrect CVV |
| `86` | PIN Present but Not Verified |
| `91` | STIP default response code from Country-to-Country Exclusion Table |
| `91` | Decline Generic EMV transactions in STIP |
| `91` | Decline in STIP Non-domestic Key Entered transactions |
| `91` | Decline in STIP Card Not Present transactions |
| `91` | Decline in STIP PIN-present transactions |
| `91` | Decline in STIP PIN-not-present transactions |
| `91` | Decline in STIP transactions above USD 100,000 |
| `91` | Decline in International transactions |
| `N7` | CVV2 STIP Default Response Code |
| `R0` | Decline in STIP — Stop Payment Order |
| `R1` | Decline in STIP — Revocation Of One Authorization Order |
| `R3` | Decline in STIP — Revocation Of All Authorization Order |
## Hard Decline Codes [#hard-decline-codes]
Hard declines are permanent failures that will not succeed on retry. These require customer intervention.
| Response Code | Definition |
|---|---|
| `00` | Processed in STIP / Generic Send to STIP with no Error |
| `01` | CVV Default Response Code — Refer to Issuer |
| `02` | Refer to Issuer — Special Condition |
| `04` | CVV Default Response Code — Pick Up Card |
| `05` | CVV Default Response Code — Decline |
| `05` | CAVV validation failed |
| `05` | WLM Scoring Request Decline |
| `05` | Targeted Acceptance Decline |
| `05` | CAVCS transaction decline |
| `05` | CAM failed (CAM — All respond) |
| `05` | MIT Timeline failure |
| `12` | Invalid transaction |
| `12` | Bill Payment not supported for this country, MCC, or Network |
| `12` | Token auth/prov environ mismatch |
| `13` | Currency Conversion overflow / amount invalid |
| `14` | Invalid PAN length |
| `14` | Invalid Check Digit |
| `15` | No Issuer for Account Number |
| `19` | Merchant fraud risk assessment |
| `21` | Delayed Settlement |
| `51` | Insufficient Funds |
| `54` | Expired Card |
| `55` | Incorrect PIN |
| `55` | PIN Missing |
| `57` | Invalid Service Code |
| `57` | Transaction Type and/or MCG blocked for issuer |
| `57` | Issuer Blocked |
| `57` | Acquirer Blocked |
| `57` | Sanction Bin Blocking - decline |
| `58` | Transaction not allowed in acquirer terminal |
| `59` | RTD Decline |
| `61` | Money Transfer Amount Limit exceeded |
| `62` | Restricted Country for card usage |
| `62` | MIT Timeline failure |
| `64` | Transaction does not fulfill AML requirement |
| `65` | Money Transfer Amount Limit exceeded |
| `75` | "PIN Incorrect" activity threshold exceeded |
| `76` | Unsolicited Reversal |
| `81` | PIN decryption error |
| `82` | Incorrect CVV or iCVV |
| `83` | Unable to verify PIN |
| `85` | Account Verification — No Reason to Decline |
| `86` | Could not Verify PIN |
| `91` | Destination unavailable |
| `93` | Transaction could not be completed — Violation Of Law |
| `94` | Duplicate transmission |
| `96` | I/O Error retrieving CDB record |
| `N5` | Not eligible for resubmission |
| `N7` | CVV2 Failed |
| `N8` | Transaction amount in Completion exceeds that in Pre-Auth |
| `R0` | Stop Payment Order |
| `R1` | Revocation of Authorization Order |
| `R3` | Revocation of all Auth Order |
## Common Response Codes Quick Reference [#common-response-codes-quick-reference]
| Code | Category | Meaning | Recommended Action |
|---|---|---|---|
| `00` | Approved | Transaction approved | None required |
| `01` | Soft | Refer to issuer | Retry or contact issuer |
| `05` | Hard | Do not honor | Use different card |
| `12` | Hard | Invalid transaction | Check transaction data |
| `14` | Hard | Invalid card number | Verify card details |
| `51` | Hard | Insufficient funds | Customer to add funds or use different card |
| `54` | Hard | Expired card | Use valid card |
| `55` | Hard | Incorrect PIN | Re-enter PIN |
| `57` | Hard | Transaction not permitted | Use different card/method |
| `61` | Soft | Exceeds withdrawal limit | Retry with lower amount or later |
| `65` | Soft | Activity limit exceeded | Retry later |
| `91` | Soft | Issuer unavailable | Retry later |
## Retry Logic Recommendations [#retry-logic-recommendations]
### Soft Declines [#soft-declines]
- **Wait and retry**: Allow 15-30 minutes before retrying
- **Maximum attempts**: Limit to 3 retry attempts per 24-hour period
- **Exponential backoff**: Increase wait time between retries
### Hard Declines [#hard-declines]
- **Do not retry automatically**: These require customer action
- **Notify customer**: Provide clear instructions on next steps
- **Request alternative payment**: Prompt for a different payment method
### Special Cases [#special-cases]
| Code | Handling |
|---|---|
| `51` (Insufficient Funds) | Allow customer-initiated retry after they've added funds |
| `54` (Expired Card) | Request updated card details |
| `91` (Issuer Unavailable) | Implement automatic retry with exponential backoff |
| `65` (Activity Limit) | Wait until the next calendar day to retry |
## Integration Notes [#integration-notes]
1. **Always log the F39 code**: Store the `response_code` for all transactions for debugging and analytics
2. **Don't rely solely on mapped errors**: The `processing_status` and `error` fields contain mapped values that may lose specificity
3. **Build response code handling**: Implement specific handling based on F39 codes rather than generic error categories
4. **Monitor trends**: Track response code frequency to identify issues with specific card types or issuers
# Reconciliation (/docs/server-to-server/reconciliation)
## Introduction [#introduction]
The Precium Reconciliation Service provides automated transaction reconciliation between your payment processing activity and settlement data. This service generates daily report files that enable you to track transactions through the complete payment lifecycle, from authorization to bank settlement.
This documentation covers the structure, format, and interpretation of reconciliation report files to ensure accurate financial reporting and streamlined operations.
## Report Types Overview [#report-types-overview]
The reconciliation system produces three distinct report types, each serving a specific purpose in the reconciliation workflow:
| Report Type | Purpose | Settlement Required |
|---|---|---|
| **Recon File** | Primary reconciliation output with full settlement data | Yes |
| **Markoff File** | Transaction activity reconciled between gateways and providers | No |
| **Exception File** | Transactions that could not be reconciled within expected timeframe | Partial data |
### When to Use Each Report [#when-to-use-each-report]
```mermaid
flowchart TD
A[Transaction Processed] --> B{Reconciled with Provider?}
B -->|Yes| C[MARKOFF FILE - Transaction activity]
B -->|No| D[Not in any report yet]
C --> E{Settlement Data Available?}
E -->|Yes| F[RECON FILE - Ready for settlement]
E -->|No| G[EXCEPTION FILE - Requires investigation]
```
## File Format Specifications [#file-format-specifications]
### Common Rules [#common-rules]
All reconciliation files follow these formatting standards:
| Attribute | Specification |
|---|---|
| **Encoding** | UTF-8 |
| **Header Row** | Always included |
| **Delimiter** | Comma (`,`) |
| **Escape Character** | Double quotes (`"`) for values containing commas |
| **Line Terminator** | CRLF (`\r\n`) |
| **Timezone** | UTC |
### Data Type Formats [#data-type-formats]
| Data Type | Format | Example |
|---|---|---|
| **Date** | `YYYY-MM-DD` | `2026-01-15` |
| **Datetime** | `YYYY-MM-DD HH:MM` | `2026-01-15 14:30` |
| **Decimal** | `#.####` (4 decimal places) | `1234.5678` |
| **UUID** | 36-character string | `a1b2c3d4-e5f6-7890-abcd-ef1234567890` |
| **Boolean** | `True` / `False` | `True` |
| **Currency** | ISO 4217 | `ZAR`, `USD`, `EUR` |
| **Country** | ISO 3166-1 alpha-2 | `ZA`, `US`, `GB` |
### Special Formatting Notes [#special-formatting-notes]
**Numeric Strings:** Values intended to be interpreted as strings (not numbers) are prepended with a single quote (`'`) to prevent spreadsheet applications from reformatting them.
```text
'0012345678,Card Reference,...
```
**Empty Values:** Fields without data are represented as empty strings between delimiters:
```text
field1,field2,,field4
```
**Multi-Value Fields:** Some fields (like `bank_statement_line_item`) may contain multiple values separated by commas within the quoted field:
```text
"value1,value2,value3"
```
## Recon file fields [#recon-file-fields]
The field tables that were here described **Recon File Specification v1.0**.
The current file is **v3.0**, and almost every column was renamed between the
two. `purchase_id` is now `gateway_transaction_id`, `auth_code` is now
`transaction_auth_code`, and `settlement_id` and `related_to_purchase` no
longer exist.
The current schema, the parsing rules and a version mapping are on
[Reconciliation files](/docs/reconciliation).
## Markoff File [#markoff-file]
### Description [#description]
The Markoff File contains transaction data that has been reconciled between Precium's gateways and payment providers. This file represents all transaction actions within the reporting timeframe, regardless of settlement status.
**Key Characteristics:**
- Settlement to Precium is not required for inclusion
- Serves as a transaction activity log
- Useful for tracking transactions before settlement data is available
### Use Cases [#use-cases]
- Daily transaction activity monitoring
- Pre-settlement transaction verification
- Operational reporting
- Transaction volume analysis
### Field Specification [#field-specification]
The Markoff File shares the same field structure as the Recon File across Merchant & Brand Identification, Transaction Identification, Precium Invoicing, Provider & Acquirer Information, Transaction Details, Settlement Information, Settlement Amounts, and Internal Revenue Tracking sections.
Note: The Markoff File does not include Bank Statement Reconciliation fields.
## Exception File [#exception-file]
### Description [#description-1]
The Exception File contains transaction records that could not be reconciled successfully within the normal or expected reconciliation timeframe.
**Key Characteristics:**
- Contains transactions with incomplete reconciliation
- Data is populated on a best-effort basis (partial data expected)
- May require manual investigation or subsequent automated processing
### Use Cases [#use-cases-1]
- Identifying reconciliation issues
- Manual investigation triggers
- Tracking unresolved transactions
### Field Specification [#field-specification-1]
The Exception File includes all standard fields plus extended Precium Invoicing fields:
| Field | Type | Format | Description |
|---|---|---|---|
| `precium_invoiced_vat` | decimal | `#.####` | VAT portion of invoiced amount |
| `precium_invoiced_incl_vat` | decimal | `#.####` | Total invoiced including VAT |
It also uses `service_type` instead of `card_type` and `projected_settlement_date` instead of `projected_clearance_date`.
## Working with Reconciliation Files [#working-with-reconciliation-files]
### File Naming Convention [#file-naming-convention]
Reconciliation files follow a consistent naming pattern:
```text
{report_type}_{merchant_id}_{date}.csv
```
Examples:
```text
recon_a1b2c3d4-e5f6-7890-abcd-ef1234567890_2026-01-15.csv
markoff_a1b2c3d4-e5f6-7890-abcd-ef1234567890_2026-01-15.csv
exception_a1b2c3d4-e5f6-7890-abcd-ef1234567890_2026-01-15.csv
```
### Importing into Spreadsheet Applications [#importing-into-spreadsheet-applications]
When importing reconciliation files into Excel or Google Sheets:
1. Use UTF-8 encoding when importing
2. Treat UUID fields as text to prevent truncation
3. Note the single-quote prefix on numeric strings (Excel will display without the quote)
4. Set decimal columns to 4 decimal places for accurate monetary values
### Sample CSV structure [#sample-csv-structure]
```text
merchant_name,merchant_id,brand_name,brand_id,settlement_acquiring_bank,settlement_bank_account_number,settlement_date,gateway_transaction_id,gateway_transaction_reference,transaction_amount,transaction_currency,transaction_type,...
Acme Corp,a1b2c3d4-e5f6-7890-abcd-ef1234567890,Acme Online,b2c3d4e5-f6a7-8901-bcde-f12345678901,ABSA,'62012345678,2026-09-22,a1b2c3d4-0001-4a1a-9e16-000000000001,'WWILJH1789639210276600001,40.0000,ZAR,PURCHASE,...
```
Full column list on [Reconciliation files](/docs/reconciliation).
### Daily Reconciliation Workflow [#daily-reconciliation-workflow]
```mermaid
flowchart LR
subgraph T0["T+0 Morning"]
A1[Transactions processed]
A2[Data captured]
end
subgraph Night["T+0 to T+1 Overnight"]
B1[Gateway-provider recon]
B2[Settlement processing]
B3[Bank statement matching]
end
subgraph T1["T+1 Morning"]
C1[Recon File]
C2[Markoff File]
C3[Exception File]
end
subgraph Ongoing["Ongoing"]
D1[Investigation]
D2[Manual recon]
D3[Resolution]
end
T0 --> Night --> T1 --> Ongoing
D3 -.->|Resolved| C1
```
### Handling refunds and disputes [#handling-refunds-and-disputes]
A refund or a chargeback is a separate payment transaction with its own
`gateway_transaction_id`, RRN and authorisation code, and its own row carrying a
negative `transaction_amount`.
```text
gateway_transaction_id,gateway_transaction_reference,transaction_type,transaction_amount
a1b2c3d4-0002-4a1a-9e16-000000000002,'EAQSFU1789639185638600002,PURCHASE,100.0000
a1b2c3d4-0003-4a1a-9e16-000000000003,'12442,REFUND,-30.0000
```
Recon v2.1 linked a refund back to its purchase through
`related_transaction_reference`. **That column does not exist in v3.0.** Record
the linkage on your side when you make the refund call.
**Transaction types**
| `transaction_type` | Description |
|---|---|
| `PURCHASE` | Original payment transaction |
| `REFUND` | Full or partial refund |
| `dispute` | Chargeback or dispute |
| `reversal` | Transaction reversal |
### Settlement Amount Calculations [#settlement-amount-calculations]
Understanding the relationship between settlement fields:
```text
gross_settlement_amount
- precium_settlement_deduction_incl_vat
- settlement_provider_fees
- acquirer_settlement_deduction_incl_vat
═════════════════════════════════════════
= net_settlement_amount
```
**Example:**
| Field | Amount |
|---|---|
| `gross_settlement_amount` | 1,000.0000 |
| `precium_settlement_deduction_incl_vat` | -25.0000 |
| `settlement_provider_fees` | -15.0000 |
| `acquirer_settlement_deduction_incl_vat` | -10.0000 |
| **`net_settlement_amount`** | **950.0000** |
## Troubleshooting [#troubleshooting]
### Transaction Missing from Recon File [#transaction-missing-from-recon-file]
**Possible Causes:**
- Settlement data not yet received from the provider
- Transaction still in processing
- Reconciliation exception occurred
**Resolution:**
1. Check the Markoff File to confirm the transaction was processed
2. Check the Exception File for reconciliation issues
3. Wait for the next reconciliation cycle
4. Contact support if the transaction remains missing after 3 business days
### Amounts Don't Match [#amounts-dont-match]
**Possible Causes:**
- Currency conversion differences
- Fee calculation timing
- Partial refund applied
**Resolution:**
1. Verify transaction and settlement currencies match
2. Match a refund to its purchase using the linkage you recorded at refund time
3. Review fee deduction fields
### Exception File Contains Expected Transactions [#exception-file-contains-expected-transactions]
**Possible Causes:**
- Provider settlement file delayed
- Bank statement not yet processed
- Data mismatch requiring investigation
**Resolution:**
1. Transactions typically clear within 1-3 business days
2. Check subsequent Recon Files for resolution
3. Contact support for persistent exceptions
### Data Validation Checklist [#data-validation-checklist]
When processing reconciliation files, validate:
- File encoding is UTF-8
- All UUIDs are 36 characters
- Dates follow `YYYY-MM-DD` format
- Datetimes follow `YYYY-MM-DD HH:MM` format
- Decimal values have up to 4 decimal places
- Currency codes are valid ISO 4217
- Country codes are valid ISO 3166-1 alpha-2
- Boolean fields contain only `True` or `False`
## API Access [#api-access]
Reconciliation files can be retrieved programmatically via the Precium API.
### List Available Reports [#list-available-reports]
```bash
curl -X GET "https://gate.reviopay.com/api/v1/reconciliation/reports/" \
-H "Authorization: Bearer YOUR_API_KEY"
```
**Response:**
```json
{
"reports": [
{
"id": "rpt_abc123",
"type": "recon",
"date": "2026-01-15",
"merchant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"file_name": "recon_a1b2c3d4_2026-01-15.csv",
"download_url": "https://...",
"created_at": "2026-01-16T06:00:00Z"
}
]
}
```
### Download Report [#download-report]
```bash
curl -X GET "https://gate.reviopay.com/api/v1/reconciliation/reports/{report_id}/download/" \
-H "Authorization: Bearer YOUR_API_KEY" \
-o report.csv
```
### Filter by Date Range [#filter-by-date-range]
```bash
curl -X GET "https://gate.reviopay.com/api/v1/reconciliation/reports/?start_date=2026-01-01&end_date=2026-01-15&type=recon" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Support [#support]
For questions about reconciliation reports or to report discrepancies:
- **Email:** support@precium.com
- **Subject Line:**`[Recon] {Merchant Name} - {Issue Description}`
- **Include:** Merchant ID, affected date(s), purchase ID(s), and description of issue
# Technical reference documentation (/docs/server-to-server/technical-reference-documentation)
## About this reference [#about-this-reference]
The endpoint and field reference for the Precium card API. Interactive documentation and ready-to-run request collections are linked below; the tables on this page are the quick reference for the card surface.
| Resource | Link |
|---|---|
| **Interactive API reference** | [gate.reviopay.com/api/](https://gate.reviopay.com/api/) |
| **Postman collection** | Server-to-server merchant samples — request access from [support@precium.com](mailto:support@precium.com) |
If you cannot open the Postman collection, contact support rather than assuming you lack permission — access is granted per workspace member.
## Base URL [#base-url]
```text
https://gate.reviopay.com/api/v1/
```
Every path below is relative to that base. Test and production share this URL; the API key you send selects the environment.
## Authentication [#authentication]
Bearer token on every request. Two credentials, from two different places.
| Key | Scope |
|---|---|
| **Bearer token** | All endpoints on this page. Developers section of your dashboard |
| **S2S token** | Card data submission to a purchase's `direct_post_url` only. Issued by Precium to qualifying merchants, **not on the dashboard** |
```text
Authorization: Bearer
```
Creating a purchase also requires your `brand_id`.
## Purchases [#purchases]
| Method | Path | Purpose |
|---|---|---|
| POST | `/purchases/` | Create a purchase — the primary request for any integration |
| GET | `/purchases/{id}/` | Retrieve a purchase, including `transaction_data.attempts[]` |
| POST | `/purchases/{id}/charge/` | Charge or hold using a saved token |
| POST | `/purchases/{id}/capture/` | Capture a previously authorised payment |
| POST | `/purchases/{id}/release/` | Release funds on hold |
| POST | `/purchases/{id}/refund/` | Refund a paid purchase, fully or partially |
| POST | `/purchases/{id}/cancel/` | Cancel a pending purchase |
| POST | `/purchases/{id}/mark_as_paid/` | Mark a purchase paid — for payments settled outside Precium |
| POST | `/purchases/{id}/delete_recurring_token/` | Delete the token stored against this purchase |
| POST | `/purchases/{id}/resend_invoice/` | Re-send the invoice email |
### Creating a purchase — request fields [#creating-a-purchase--request-fields]
Three fields are required: a client (as `client_id` or an inline `client` object), `purchase`, and `brand_id`.
| Field | Type | Required | Notes |
|---|---|---|---|
| `brand_id` | string (uuid) | **Yes** | From your dashboard |
| `client_id` | string (uuid) | Either this… | An existing client |
| `client` | object | …or this | Inline client; found or created by `email` |
| `purchase` | object | **Yes** | Container for `currency`, `products`, `language` |
| `purchase.currency` | string | **Yes** | ISO 4217, e.g. `ZAR` |
| `purchase.products` | array | **Yes** | Each item needs `name` and `price` |
| `purchase.products[].price` | integer | **Yes** | **In cents.** 149900 = R1,499.00 |
| `reference` | string | No | Your own order identifier |
| `skip_capture` | boolean | No | Authorise only. With a zero total, becomes preauthorisation |
| `force_recurring` | boolean | No | Store the card; the purchase id becomes the token |
| `payment_method_whitelist` | array | No | Restrict accepted methods |
| `send_receipt` | boolean | No | Email a receipt on payment |
| `success_redirect` | string (url) | No* | *Required together with `failure_redirect` for direct post |
| `failure_redirect` | string (url) | No* | See above |
| `cancel_redirect` | string (url) | No | Where a cancelled payment returns |
| `success_callback` | string (url) | No | Per-purchase callback. Verify with `GET /public_key/` |
| `due` | integer | No | Due date, Unix timestamp |
| `purchase.due_strict` | boolean | No | Block payment after the due date |
**Booleans must be sent as booleans**, not strings — `true`, not `"true"`. The same applies to amounts: integers, not quoted numbers.
### Response fields worth keeping [#response-fields-worth-keeping]
| Field | Notes |
|---|---|
| `id` | The purchase id. Also the card token when `is_recurring_token` is true |
| `status` | See the lifecycle table in the *Integration guide* |
| `checkout_url` | Precium-hosted payment page |
| `direct_post_url` | Card submission endpoint. Null unless both redirects were supplied |
| `is_recurring_token` | True once a card has been stored against this purchase |
| `refund_availability` | `all`, `full_only`, `partial_only`, `pis_all`, `pis_partial` or `none` |
| `refundable_amount` | Amount still available to refund |
| `transaction_data.attempts[]` | Attempt history, newest first. `.error` carries the failure code |
| `marked_as_paid` | True if settled via `/mark_as_paid/` rather than through the gateway |
## Direct post fields [#direct-post-fields]
Posted to a purchase's `direct_post_url`. Add `?s2s=true` for server-side submission with the S2S key.
| Field | Format |
|---|---|
| `pm` | Payment method, e.g. `visa` |
| `cardholder_name` | Latin letters, space, apostrophe, dot, dash. Max 30 |
| `card_number` | Digits only, no whitespace. Max 19 |
| `expires` | `MM/YY` — two digits, slash, two digits |
| `cvc` | 3 or 4 digits |
| `remember_card` | `on` to store the card |
Validation failures are returned as payment failures, so validate client-side first.
## Clients [#clients]
| Method | Path | Purpose |
|---|---|---|
| POST | `/clients/` | Create a client. `email` required and unique |
| GET | `/clients/` | List clients. Filter with `q`, `email` or `phone` |
| GET | `/clients/{id}/` | Retrieve a client |
| PUT / PATCH | `/clients/{id}/` | Replace or partially update |
| DELETE | `/clients/{id}/` | Delete a client |
| GET | `/clients/{id}/feed/` | Purchases and refunds for this client |
| GET | `/clients/{id}/recurring_tokens/` | Stored cards |
| GET / DELETE | `/clients/{id}/recurring_tokens/{token_id}/` | Retrieve or remove a stored card |
Client fields include `email`, `full_name`, `phone`, `personal_code` (identity number, used for DebiCheck), billing and shipping address fields, `bank_account`, `bank_code`, and `cc` / `bcc` arrays for receipt copies.
## Payment methods [#payment-methods]
```bash
GET https://gate.reviopay.com/api/v1/payment_methods/?brand_id={brand_id}¤cy=ZAR
```
Returns the methods actually enabled for that brand and currency. Send it with the same key you will create the purchase with — the key determines whether the lookup runs against test or live configuration. Prefer this over a hard-coded list.
## Webhooks [#webhooks]
| Method | Path | Purpose |
|---|---|---|
| POST | `/webhooks/` | Create a webhook. Requires `title`, `callback`, `events` |
| GET | `/webhooks/` | List webhooks |
| GET | `/webhooks/{id}/` | Retrieve a webhook, including its `public_key` |
| PUT / PATCH | `/webhooks/{id}/` | Replace or partially update |
| DELETE | `/webhooks/{id}/` | Delete a webhook |
| GET | `/webhooks/deliveries/` | Delivery history for one object |
`/webhooks/deliveries/` requires both `id` and `source_type` — the latter being `Purchase`, `Payment`, `Payout` or `BillingTemplateClient`. It returns every attempt, including failures and retries.
## Callback verification [#callback-verification]
```bash
GET https://gate.reviopay.com/api/v1/public_key/
```
Returns the PEM-encoded RSA public key for verifying per-purchase `success_callback` payloads. For webhook subscriptions, use `public_key` on the webhook object instead.
Every callback carries an `X-Signature` header: a base64-encoded RSA PKCS#1 v1.5 signature of the SHA-256 digest of the raw request body. Verify the raw bytes before parsing.
## Reporting [#reporting]
| Method | Path | Purpose |
|---|---|---|
| GET | `/balance/` | Balance data |
| GET | `/account/json/balance/` | Company balance |
| GET | `/account/json/turnover/` | Company turnover |
| POST | `/company_statements/` | Schedule statement generation |
| GET | `/company_statements/` | List generated statements |
Reconciliation file formats and settlement reporting are covered in *Reconciliation*.
## Conventions [#conventions]
| Topic | Behaviour |
|---|---|
| **Amounts** | Integers, in cents, always |
| **Identifiers** | UUIDs |
| **Timestamps** | Unix seconds on request fields; ISO 8601 on responses |
| **Pagination** | Cursor-based: `results` with `next` and `previous`. Follow `next` |
| **Rate limits** | 10 transactions per second, per IP address. `429` when exceeded |
| **Idempotency** | No idempotency key. A purchase may be paid only once; repeats return `409` |
| **Trailing slashes** | Required. Paths are case-sensitive — `/purchases/`, never `/Purchases/` |
## Status codes [#status-codes]
| Code | Meaning |
|---|---|
| `200` / `201` | Success / created |
| `204` | Deleted |
| `400` | Validation error, or a failed capture, release or refund — check the error code |
| `401` | Invalid or missing API key |
| `403` | Key valid but not permitted for this operation or brand |
| `404` | Not found — often a case-sensitivity or trailing-slash error |
| `409` | Conflict; typically an already-paid purchase |
| `429` | Rate limited |
| `500` | Server error. Safe to retry idempotent reads |
Full error codes with causes and retry guidance are in *Error mapping*; issuer decline codes are in *Issuer response codes*.
# Test scenarios (/docs/server-to-server/test-scenarios)
## Precium S2S Testing Checklist [#precium-s2s-testing-checklist]
This document provides a comprehensive testing checklist to ensure your integration handles all payment scenarios correctly before going live.
## Pre-Testing Setup [#pre-testing-setup]
Before running tests, ensure you have:
- Sandbox API credentials configured
- Standard API Key and S2S API Key available
- Brand ID configured for sandbox
- Webhook endpoint deployed and accessible
- Logging enabled for all API calls and responses
- Test card numbers ready (see Test Cards Reference below)
## Test Categories Overview [#test-categories-overview]
| Category | Tests | Priority |
|---|---|---|
| Happy Path | 8 | Critical |
| Pre-Authorization & Capture | 6 | Critical |
| Void/Cancel Operations | 4 | High |
| Refund Scenarios | 5 | High |
| Error Handling | 12 | Critical |
| 3DS Scenarios | 5 | Critical |
| Webhook Integration | 8 | Critical |
| Edge Cases | 7 | Medium |
| External Integrations | 4 | Conditional |
| Security & Compliance | 6 | Critical |
**Total: 65 test cases**
## 1. Happy Path Tests [#1-happy-path-tests]
### 1.1 Standard CIT Payment with Internal 3DS [#11-standard-cit-payment-with-internal-3ds]
**Test Card:**`4000000000001091` (3DS required)
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create client | Returns client ID |
| 2 | Create purchase (amount: 10000, force_recurring: true) | Returns purchase ID and direct_post_url |
| 3 | Submit card data with browser fingerprint | Returns 3DS parameters or ready status |
| 4 | Complete 3DS challenge | Redirects to callback URL |
| 5 | POST to callback with MD and PaRes | Returns success |
| 6 | Verify purchase status | Status: "paid" |
| 7 | Check for token | payment_method_details.card.token present |
| 8 | Check for network_transaction_id | Value present for MIT use |
### 1.2 Standard CIT Payment without 3DS Challenge [#12-standard-cit-payment-without-3ds-challenge]
**Test Card:**`4000000000001000` (No 3DS)
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create client | Returns client ID |
| 2 | Create purchase | Returns purchase ID |
| 3 | Submit card data | No 3DS parameters returned |
| 4 | Charge purchase | Status: "paid" |
### 1.3 Preauthorisation (Card Validation) [#13-preauthorisation-card-validation]
**Test Card:**`4000000000001091`
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create client | Returns client ID |
| 2 | Create purchase with amount: 0 | Returns purchase ID |
| 3 | Submit card data | 3DS flow initiated |
| 4 | Complete 3DS | Success |
| 5 | Verify no charge | Amount authorized: 0 |
| 6 | Verify token generated | Token present (if force_recurring: true) |
### 1.4 MIT Recurring Charge [#14-mit-recurring-charge]
**Prerequisite:** Completed CIT with token stored
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create MIT purchase with token reference | Returns purchase ID |
| 2 | Include previous_network_transaction_id | Accepted |
| 3 | Include original_amount_cents | Accepted |
| 4 | Submit with stored token (no CVV) | Ready for charge |
| 5 | Charge purchase | Status: "paid" |
| 6 | Verify no 3DS required | No 3DS challenge |
### 1.5 Mastercard Payment [#15-mastercard-payment]
**Test Card:**`5555555555554444`
| Step | Action | Expected Result |
|---|---|---|
| 1 | Complete standard flow | Success |
| 2 | Verify card brand in response | brand: "mastercard" |
### 1.6 Multiple Products in Single Purchase [#16-multiple-products-in-single-purchase]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create purchase with multiple products | Total calculated correctly |
| 2 | Complete payment | Full amount charged |
### 1.7 Payment with Metadata [#17-payment-with-metadata]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create purchase with metadata | Accepted |
| 2 | Complete payment | Success |
| 3 | Retrieve purchase | Metadata preserved |
| 4 | Receive webhook | Metadata included |
### 1.8 Different Currency (if supported) [#18-different-currency-if-supported]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create purchase with USD currency | Accepted |
| 2 | Complete payment | Charged in USD |
## 2. Pre-Authorization & Capture Tests [#2-pre-authorization--capture-tests]
### 2.1 Pre-Auth with Full Capture [#21-pre-auth-with-full-capture]
**Test Card:**`4000000000001091`
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create purchase (amount: 50000) | Returns purchase ID |
| 2 | Submit card and complete 3DS | Authorization successful |
| 3 | Verify status | Status: "authorized" |
| 4 | Capture full amount | Status: "paid" |
| 5 | Verify captured amount | amount: 50000 |
### 2.2 Pre-Auth with Partial Capture [#22-pre-auth-with-partial-capture]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create and authorize (amount: 50000) | Status: "authorized" |
| 2 | Capture partial amount (35000) | Status: "paid" |
| 3 | Verify captured amount | amount: 35000 |
| 4 | Verify remaining released | No hold for remaining 15000 |
### 2.3 Pre-Auth with Multiple Partial Captures [#23-pre-auth-with-multiple-partial-captures]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Authorize 100000 | Status: "authorized" |
| 2 | First capture: 30000 | Success, remaining: 70000 |
| 3 | Second capture: 40000 | Success, remaining: 30000 |
| 4 | Third capture: 30000 | Success, fully captured |
| 5 | Fourth capture attempt | Error: nothing remaining |
### 2.4 Pre-Auth without CVV (Token-based) [#24-pre-auth-without-cvv-token-based]
**Prerequisite:** Stored token from previous CIT
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create purchase with is_recurring: true | Accepted |
| 2 | Submit with token, CVV: "000" | Accepted |
| 3 | Authorization completes | Status: "authorized" |
| 4 | Capture | Status: "paid" |
### 2.5 Capture Exceeding Authorization [#25-capture-exceeding-authorization]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Authorize 50000 | Status: "authorized" |
| 2 | Attempt capture of 60000 | Error: capture_amount_exceeds_authorization |
### 2.6 Authorization Expiry Handling [#26-authorization-expiry-handling]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Note: May require waiting or test mode | — |
| 2 | Attempt capture on expired auth | Error: authorization_expired |
| 3 | Verify appropriate error handling | User-friendly message |
## 3. Void/Cancel Tests [#3-voidcancel-tests]
### 3.1 Void Pre-Authorization [#31-void-pre-authorization]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create and authorize purchase | Status: "authorized" |
| 2 | Call cancel endpoint | Status: "cancelled" |
| 3 | Verify funds released | No hold on card |
| 4 | Attempt capture after void | Error: already_voided |
### 3.2 Void Capture (Same Day, Before Settlement) [#32-void-capture-same-day-before-settlement]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Complete payment | Status: "paid" |
| 2 | Void same day | Status: "cancelled" |
| 3 | Verify immediate reversal | Funds released |
### 3.3 Void After Settlement (Should Fail) [#33-void-after-settlement-should-fail]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Attempt void on settled transaction | Error: void_not_permitted |
| 2 | Verify error message | Suggests using refund |
### 3.4 Refund is irreversible [#34-refund-is-irreversible]
A refund cannot be voided. Confirm that your own interface prevents a second submission, since there is no idempotency key and a retried request is a second refund.
| Step | Action | Expected Result |
|---|---|---|
| 1 | Process a partial refund | Payment object returned; `refundable_amount` reduced |
| 2 | Attempt to reverse it | No such operation exists — confirm your code never offers one |
| 3 | Re-read the purchase | `refundable_amount` reflects the refund; original charge intact |
| 4 | Submit the same refund twice | Second request is a second refund — confirm your UI blocks it |
## 4. Refund Scenarios [#4-refund-scenarios]
### 4.1 Full Refund [#41-full-refund]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Complete payment (29900) | Status: "paid" |
| 2 | Request full refund | Status: "refunded" |
| 3 | Verify refund amount | amount: 29900 |
| 4 | Receive webhook | Event: purchase.refunded |
### 4.2 Partial Refund [#42-partial-refund]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Complete payment (50000) | Status: "paid" |
| 2 | Request partial refund (20000) | Success |
| 3 | Verify remaining | 30000 still charged |
| 4 | Verify refund details | Refund ID, amount: 20000 |
### 4.3 Multiple Partial Refunds [#43-multiple-partial-refunds]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Complete payment (100000) | Status: "paid" |
| 2 | First refund (25000) | Success, remaining: 75000 |
| 3 | Second refund (25000) | Success, remaining: 50000 |
| 4 | Third refund (50000) | Success, remaining: 0 |
| 5 | Fourth refund attempt | Error: nothing to refund |
### 4.4 Refund Exceeding Original Amount [#44-refund-exceeding-original-amount]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Complete payment (30000) | Status: "paid" |
| 2 | Request refund (40000) | Error: exceeds original |
### 4.5 Refund Exceeding Remaining Amount [#45-refund-exceeding-remaining-amount]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Complete payment (50000) | Status: "paid" |
| 2 | First refund (30000) | Success |
| 3 | Second refund (30000) | Error: exceeds remaining |
## 5. Error Handling Tests [#5-error-handling-tests]
### 5.1 Card Declined - Generic [#51-card-declined---generic]
**Test Card:**`4000000000000002`
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit declined card | Error returned |
| 2 | Verify error code | do_not_honour |
| 3 | Verify user-friendly message | Appropriate message |
### 5.2 Expired Card [#52-expired-card]
**Test Card:**`4000000000000069`
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit expired card | Error: expired_card |
| 2 | Verify message | "Card has expired" |
### 5.3 Insufficient Funds [#53-insufficient-funds]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit card with insufficient funds | Error: insufficient_funds |
| 2 | Verify soft decline | Can retry later |
### 5.4 Invalid CVC [#54-invalid-cvc]
**Test Card:**`4000000000000127`
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit with wrong CVC | Error: cvc_check_failed |
### 5.5 Invalid Card Number [#55-invalid-card-number]
**Test Input:**`4111111111111112` (fails Luhn)
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit invalid card number | Error: invalid_card_number |
| 2 | Verify validation message | Clear error message |
### 5.6 Invalid Expiry Format [#56-invalid-expiry-format]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit wrong format | Error: invalid_expires |
| 2 | Test: "2028/12", "1228" | All fail |
| 3 | Correct: "12/28" | Success |
### 5.7 Processing Error [#57-processing-error]
**Test Card:**`4000000000000119`
| Step | Action | Expected Result |
|---|---|---|
| 1 | Trigger processing error | Error returned |
| 2 | Verify retry logic | Exponential backoff |
### 5.8 Missing Required Fields [#58-missing-required-fields]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit without client_id | Validation error |
| 2 | Submit without brand_id | Validation error |
| 3 | Submit without currency | Validation error |
| 4 | Submit without products | Validation error |
### 5.9 Invalid API Key [#59-invalid-api-key]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Use invalid API key | HTTP 401 |
| 2 | Use wrong key type | Error returned |
| 3 | Use expired key | HTTP 401 |
### 5.10 Network Timeout Handling [#510-network-timeout-handling]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Simulate network timeout | Error caught |
| 2 | Verify retry logic | Retries with backoff |
| 3 | Verify idempotency | No duplicate charges |
### 5.11 Duplicate Charge Prevention [#511-duplicate-charge-prevention]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit charge request | Success |
| 2 | Resubmit same request | Idempotent response |
| 3 | Verify only one charge | Single transaction |
### 5.12 Expired Direct Post URL [#512-expired-direct-post-url]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create purchase | Get direct_post_url |
| 2 | Use URL once | Success |
| 3 | Reuse URL | Error: expired |
## 6. 3DS Scenarios [#6-3ds-scenarios]
### 6.1 3DS Challenge Flow (Internal) [#61-3ds-challenge-flow-internal]
**Test Card:**`4000000000001091`
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit card requiring 3DS | 3DS parameters returned |
| 2 | Verify 3DS fields | MD, PaReq, URL, callback_url |
| 3 | Redirect to 3DS URL | Bank page loads |
| 4 | Complete challenge | Redirects to callback |
| 5 | Submit callback | Payment completes |
### 6.2 3DS Authentication Failed [#62-3ds-authentication-failed]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Initiate 3DS | Challenge presented |
| 2 | Fail authentication | Error: 3ds_authentication_failed |
| 3 | Verify retry available | Can retry |
### 6.3 3DS Timeout [#63-3ds-timeout]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Initiate 3DS | Challenge presented |
| 2 | Allow timeout | Error: timeout |
| 3 | Verify graceful handling | User-friendly message |
### 6.4 Frictionless 3DS [#64-frictionless-3ds]
**Test Card:**`4000000000001000`
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit low-risk transaction | No challenge |
| 2 | Verify frictionless | Success without redirect |
### 6.5 3DS Data in Response [#65-3ds-data-in-response]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Complete 3DS transaction | Success |
| 2 | Verify ECI value | Present |
| 3 | Verify CAVV | Present |
## 7. Webhook Tests [#7-webhook-tests]
### 7.1 Receive purchase.paid Event [#71-receive-purchasepaid-event]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Complete payment | Webhook received |
| 2 | Verify event type | purchase.paid |
| 3 | Verify payload | All fields present |
| 4 | Verify purchase_id | Matches transaction |
### 7.2 Receive purchase.payment_failure Event [#72-receive-purchasepayment_failure-event]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit declined card | Payment fails |
| 2 | Receive webhook | purchase.payment_failure |
| 3 | Verify error details | Error code included |
### 7.3 Receive purchase.refunded Event [#73-receive-purchaserefunded-event]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Process refund | Completes |
| 2 | Receive webhook | purchase.refunded |
| 3 | Verify details | Amount, refund_id |
### 7.4 Receive purchase.preauthorized Event [#74-receive-purchasepreauthorized-event]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Complete pre-auth | Holds funds |
| 2 | Receive webhook | purchase.preauthorized |
| 3 | Verify details | Amount, expiry |
### 7.5 Signature Verification - Valid [#75-signature-verification---valid]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Receive webhook | Headers present |
| 2 | Verify signature | Matches computed |
| 3 | Verify timestamp | Within 5 minutes |
| 4 | Process webhook | Success |
### 7.6 Signature Verification - Invalid [#76-signature-verification---invalid]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Receive tampered webhook | — |
| 2 | Verify signature | Mismatch detected |
| 3 | Reject webhook | HTTP 401 |
| 4 | No processing | No action taken |
### 7.7 Duplicate Webhook Handling [#77-duplicate-webhook-handling]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Receive webhook | Process |
| 2 | Receive same again | Detect duplicate |
| 3 | Return HTTP 200 | Acknowledge |
| 4 | Do not reprocess | Idempotent |
### 7.8 Webhook Timeout Recovery [#78-webhook-timeout-recovery]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Simulate timeout | Precium retries |
| 2 | Fix endpoint | Next retry succeeds |
| 3 | Verify no data loss | Event processed |
## 8. Edge Cases [#8-edge-cases]
### 8.1 Very Small Amount [#81-very-small-amount]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create purchase (amount: 1) | Accepted |
| 2 | Complete payment | Success |
### 8.2 Very Large Amount [#82-very-large-amount]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create large purchase | Accepted or limit error |
| 2 | Verify handling | Appropriate response |
### 8.3 Special Characters in Fields [#83-special-characters-in-fields]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Name with accents | Handled |
| 2 | Special characters | Handled |
| 3 | Verify encoding | UTF-8 preserved |
### 8.4 Token Expiry [#84-token-expiry]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Use expired token | Error: token_expired |
| 2 | Verify handling | Prompts re-tokenization |
### 8.5 Concurrent Requests [#85-concurrent-requests]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Send multiple simultaneous | All handled |
| 2 | Verify no race conditions | No duplicates |
| 3 | Verify data integrity | All correct |
### 8.6 Client Reuse [#86-client-reuse]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Create client once | ID stored |
| 2 | Use for multiple purchases | All succeed |
| 3 | Verify consistency | Data unchanged |
### 8.7 Browser Fingerprint Variations [#87-browser-fingerprint-variations]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Minimal fingerprint | Accepted |
| 2 | Full fingerprint | Accepted |
| 3 | Missing optional fields | Handled |
## 9. External Integration Tests [#9-external-integration-tests]
*Complete if using external 3DS or network tokens*
### 9.1 External 3DS (MPI) Flow [#91-external-3ds-mpi-flow]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Complete 3DS with MPI | Auth data obtained |
| 2 | Create purchase (is_external_3DS: true) | Accepted |
| 3 | Include CAVV, ECI, transaction ID | Validated |
| 4 | Complete payment | Success |
### 9.2 External 3DS - Invalid Data [#92-external-3ds---invalid-data]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit invalid CAVV | Error |
| 2 | Submit missing fields | Error |
| 3 | Submit wrong ECI format | Error |
### 9.3 Network Token Provisioning [#93-network-token-provisioning]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit network token | Accepted |
| 2 | Include cryptogram | Validated |
| 3 | Complete payment | Success |
### 9.4 Network Token - Expired Cryptogram [#94-network-token---expired-cryptogram]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit expired cryptogram | Error |
| 2 | Verify handling | Prompts refresh |
## 10. Security & Compliance Tests [#10-security--compliance-tests]
### 10.1 API Key Security [#101-api-key-security]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Check client-side code | Keys not exposed |
| 2 | Check logs | Keys masked |
| 3 | Verify HTTPS only | No HTTP |
### 10.2 Card Data Handling [#102-card-data-handling]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Check logs | Card numbers masked |
| 2 | Check storage | CVV not stored |
| 3 | Check URLs | Card data in POST only |
### 10.3 Token Storage Security [#103-token-storage-security]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Verify encryption | Encrypted at rest |
| 2 | Verify access control | Auth required |
| 3 | Verify UI | Tokens masked |
### 10.4 Webhook Endpoint Security [#104-webhook-endpoint-security]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Verify HTTPS | HTTP rejected |
| 2 | Verify signature | Invalid rejected |
| 3 | Verify timestamp | Old rejected |
### 10.5 Error Message Security [#105-error-message-security]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Check errors | No sensitive data |
| 2 | Check responses | No internal details |
| 3 | Check customer messages | Generic/friendly |
### 10.6 Rate Limiting Handling [#106-rate-limiting-handling]
| Step | Action | Expected Result |
|---|---|---|
| 1 | Exceed rate limits | HTTP 429 |
| 2 | Verify backoff | Implements retry |
| 3 | Verify recovery | Resumes normally |
## Test Cards Reference [#test-cards-reference]
### Success Cards [#success-cards]
| Card Number | Brand | 3DS | Behavior |
|---|---|---|---|
| `4000000000001091` | Visa | Yes | Success with 3DS |
| `4000000000001000` | Visa | No | Success without 3DS |
| `5555555555554444` | Mastercard | Yes | Success |
### Decline Cards [#decline-cards]
| Card Number | Error Code | Description |
|---|---|---|
| `4000000000000002` | do_not_honour | Generic decline |
| `4000000000000069` | expired_card | Expired card |
| `4000000000000127` | cvc_check_failed | Invalid CVC |
| `4000000000000119` | processing_error | Processing error |
### Test CVCs [#test-cvcs]
| CVC | Behavior |
|---|---|
| `123` | Success |
| `000` | Bypass (recurring) |
### Test Expiry [#test-expiry]
Use any future date in MM/YY format. Recommended: `12/28`
## Go-Live Checklist [#go-live-checklist]
Before going live, ensure:
- All critical tests passed
- All high-priority tests passed
- Known issues documented and accepted
- Production credentials obtained
- Production webhook URL configured
- Monitoring and alerting configured
- Support contact information documented
# Troubleshooting (/docs/server-to-server/troubleshooting)
## Precium S2S Troubleshooting Guide [#precium-s2s-troubleshooting-guide]
This guide helps you diagnose and resolve common issues when integrating with the Precium S2S API.
## Diagnostic Decision Tree [#diagnostic-decision-tree]
Use this flowchart to identify your issue category:
```mermaid
flowchart TD
A[Issue Encountered] --> B{Error in Response?}
B -->|Yes| C{HTTP Status?}
B -->|No| D{Unexpected Behavior?}
C -->|4xx| E[Client Error]
C -->|5xx| F[Server Error]
E --> G{Status Code}
G -->|400| H[Bad Request - Check payload]
G -->|401| I[Auth Error - Check API key]
G -->|403| J[Forbidden - Check permissions]
G -->|404| K[Not Found - Check endpoint/ID]
G -->|422| L[Validation - Check data format]
F --> M{Is it intermittent?}
M -->|Yes| N[Retry with backoff]
M -->|No| O[Contact support]
D -->|3DS not completing| P[3DS Issues]
D -->|Webhooks not arriving| Q[Webhook Issues]
D -->|Charges failing| R[Payment Issues]
```
## Authentication Issues [#authentication-issues]
### Issue: "401 Unauthorized" Response [#issue-401-unauthorized-response]
**Symptoms:**
```json
{
"error": "Invalid or missing API key"
}
```
**Solutions:**
| Cause | Solution |
|---|---|
| Missing Authorization header | Add `Authorization: Bearer ` to request |
| Using wrong key type | Use Standard key for most endpoints, S2S key for direct post |
| API key has whitespace | Trim whitespace from key |
| API key expired or revoked | Generate new key in dashboard |
| Using test key in production | Switch to live keys |
**Verification:**
```bash
curl -X GET https://gate.reviopay.com/api/v1/clients/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
```
### Issue: "403 Forbidden" Response [#issue-403-forbidden-response]
**Common Causes:**
- S2S not enabled for your account
- Attempting operations outside your permission scope
- IP restrictions in place
**Solution:** Contact Precium support to verify your account permissions.
## Request Validation Issues [#request-validation-issues]
### Issue: "400 Bad Request" or "422 Unprocessable Entity" [#issue-400-bad-request-or-422-unprocessable-entity]
**Check these common mistakes:**
**Missing Required Fields**
```json
// Wrong
{
"currency": "ZAR",
"products": [{"name": "Test", "price": 100}]
}
// Correct
{
"client_id": "uuid-here",
"brand_id": "your-brand-id",
"currency": "ZAR",
"products": [{"name": "Test", "price": 100}],
"success_redirect": "https://example.com/success",
"failure_redirect": "https://example.com/failure"
}
```
**Wrong Amount Format**
```javascript
// Wrong - decimal amount
{ "price": 299.00 }
// Correct - cents as integer
{ "price": 29900 }
```
**Invalid Expiry Format**
```text
Wrong: 12/2028, 2028-12, 1228
Correct: 12/28 (MM/YY format)
```
**Invalid Currency**
```text
Wrong: "Rand", "rand", "R"
Correct: "ZAR" (ISO 4217)
```
## 3DS Issues [#3ds-issues]
### Issue: 3DS Challenge Not Loading [#issue-3ds-challenge-not-loading]
```mermaid
flowchart TD
A[3DS Not Loading] --> B{Form submitted correctly?}
B -->|No| C[Fix form submission]
B -->|Yes| D{Correct fields included?}
D -->|No| E[Add MD, PaReq, TermUrl]
D -->|Yes| F{Correct target URL?}
F -->|No| G[Use 3ds.3ds_url]
F -->|Yes| H[Check browser console for errors]
```
**Required 3DS Form Fields:**
```html
```
### Issue: 3DS Callback Not Received [#issue-3ds-callback-not-received]
**Causes:**
- `callback_url` not accessible from internet
- Firewall blocking incoming requests
- URL mismatch (http vs https)
**Solution:** Ensure your callback URL is publicly accessible, uses HTTPS in production, and returns HTTP 200 within 5 seconds.
### Issue: `3ds_authentication_failed` Error [#issue-3ds_authentication_failed-error]
Allow the customer to retry. If persistent with a specific card/bank, investigate with Precium support.
## Direct Post Issues [#direct-post-issues]
### Issue: "Direct Post URL Expired" [#issue-direct-post-url-expired]
**Cause:** Direct post URLs are single-use and expire after the first submission.
**Solution:** Create a new purchase to get a fresh `direct_post_url`.
### Issue: Card Data Submission Fails [#issue-card-data-submission-fails]
| Check | Correct Format |
|---|---|
| Card number | Digits only, no spaces (e.g., `4000000000001091`) |
| Expiry | MM/YY format (e.g., `12/28`) |
| CVC | 3 digits (4 for Amex) |
| Content-Type | `application/x-www-form-urlencoded` |
| API Key | Must use S2S key, not Standard key |
**Working Example:**
```bash
curl -X POST "DIRECT_POST_URL" \
-H "Authorization: Bearer YOUR_S2S_API_KEY" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "cardholder_name=John Smith" \
-d "card_number=4000000000001091" \
-d "expires=12/28" \
-d "cvc=123" \
-d "remote_ip=196.21.45.123" \
-d "user_agent=Mozilla/5.0" \
-d "accept_header=text/html" \
-d "language=en" \
-d "java_enabled=false" \
-d "javascript_enabled=true" \
-d "color_depth=24" \
-d "utc_offset=-120" \
-d "screen_width=1920" \
-d "screen_height=1080"
```
## Charge/Payment Issues [#chargepayment-issues]
### Issue: Charge Returns Error [#issue-charge-returns-error]
```mermaid
flowchart TD
A[Charge Failed] --> B{Check error code}
B --> C{Type?}
C -->|Validation| D[Fix input data]
C -->|Auth Failure| E[3DS not completed]
C -->|Decline| F{Soft or Hard?}
C -->|Network| G[Retry later]
F -->|Soft| H[Can retry with same card]
F -->|Hard| I[Request different card]
E --> J[Complete 3DS first]
D --> K[Correct and retry]
```
## Pre-Authorization Issues [#pre-authorization-issues]
### Issue: Capture Fails After Authorization [#issue-capture-fails-after-authorization]
| Error | Cause | Solution |
|---|---|---|
| `authorization_expired` | Auth held too long | Re-authorize with customer |
| `capture_amount_exceeds_authorization` | Capturing more than authed | Reduce capture amount |
| `already_captured` | Duplicate capture | Check transaction status |
| `already_voided` | Auth was cancelled | Re-authorize |
**Diagnostic Steps:**
1. Check purchase status — must be "authorized"
2. Verify capture amount ≤ authorized amount
3. Check authorization age (7-30 days typical expiry)
```python
purchase = client.get_purchase(purchase_id)
if purchase['status'] != 'authorized':
print(f"Cannot capture - status is: {purchase['status']}")
```
### Issue: Partial Capture Not Working [#issue-partial-capture-not-working]
**Error:**`partial_capture_not_supported`
**Solution:**
- Capture the full amount and refund the difference, OR
- Void and create a new authorization for the correct amount
- Contact Precium to enable partial capture
### Issue: Pre-Authorization Void Fails [#issue-pre-authorization-void-fails]
```python
purchase = client.get_purchase(purchase_id)
if purchase['status'] == 'paid':
# Already captured - use refund
client.refund_purchase(purchase_id)
elif purchase['status'] == 'authorized':
# Can void
client.void_purchase(purchase_id)
```
## Preauthorisation Issues [#preauthorisation-issues]
### Issue: Preauthorisation Not Permitted [#issue-preauthorisation-not-permitted]
**Error:**`zero_auth_not_permitted`
**Solutions:**
1. Contact Precium to enable preauthorisation
2. Use a minimum amount (e.g., R1.00) and immediately refund
3. Use network token validation instead
### Issue: Preauthorisation Doesn't Generate Token [#issue-preauthorisation-doesnt-generate-token]
```python
purchase = client.create_zero_auth_purchase(
client_id=customer_id,
currency='ZAR',
success_redirect='https://...',
failure_redirect='https://...',
force_recurring=True # Required for tokenization
)
```
## External 3DS (MPI) Issues [#external-3ds-mpi-issues]
### Issue: External 3DS Data Rejected [#issue-external-3ds-data-rejected]
**Error:**`invalid_external_3ds_data`
| Field | Required | Format |
|---|---|---|
| `is_external_3DS` | Yes | `true` |
| `authentication_transaction_id` | Yes | Your MPI's transaction ID |
| `cavv` | Yes | Base64 encoded |
| `eci_raw` | Yes | "05", "02", "06", "01", etc. |
| `xid` | 3DS1 only | Base64 encoded |
| `ds_trans_id` | 3DS2 only | UUID format |
```python
# Wrong - missing is_external_3DS
payment_details = {
"card": {
"cavv": "AAA...",
"eci_raw": "05"
}
}
# Correct
payment_details = {
"card": {
"is_external_3DS": True, # Required
"authentication_transaction_id": "YOUR_MPI_TX_ID",
"cavv": "AAA...",
"eci_raw": "05",
"ds_trans_id": "uuid-here" # For 3DS2
}
}
```
### Issue: External 3DS Brand Not Configured [#issue-external-3ds-brand-not-configured]
**Error:**`external_3ds_not_enabled`
**Cause:** Must use a Non-3DS brand for external MPI. Contact Precium to create a Non-3DS brand.
## Network Token Issues [#network-token-issues]
### Issue: Invalid Network Token [#issue-invalid-network-token]
**Error:**`invalid_network_token`
Request fresh token from MDES/VTS and verify token requestor ID is correct.
### Issue: Cryptogram Expired [#issue-cryptogram-expired]
**Error:**`cryptogram_expired`
```python
# Request fresh cryptogram immediately before transaction
cryptogram = request_new_cryptogram_from_token_service()
# Submit transaction immediately after
```
## Webhook Issues [#webhook-issues]
### Issue: Webhooks Not Arriving [#issue-webhooks-not-arriving]
```mermaid
flowchart TD
A[Webhooks Not Arriving] --> B{URL configured in dashboard?}
B -->|No| C[Configure webhook URL]
B -->|Yes| D{Endpoint accessible?}
D -->|No| E[Check firewall/DNS]
D -->|Yes| F{SSL valid?}
F -->|No| G[Fix SSL certificate]
F -->|Yes| H{Returning 200?}
H -->|No| I[Fix endpoint code]
H -->|Yes| J[Check Precium webhook logs]
```
**Verification:**
```bash
curl -X POST https://your-webhook-url.com/precium \
-H "Content-Type: application/json" \
-d '{"test": true}'
```
### Issue: Webhook Signature Verification Fails [#issue-webhook-signature-verification-fails]
| Cause | Solution |
|---|---|
| Wrong secret | Copy fresh secret from dashboard |
| Parsed JSON instead of raw | Use raw request body for signature |
| Encoding issues | Ensure UTF-8 encoding throughout |
| Missing timestamp | Include timestamp in signature calculation |
**Debugging:**
```python
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
# Callbacks are signed with asymmetric RSA, not a shared secret.
# X-Signature = base64( RSA PKCS#1 v1.5 over the SHA-256 digest of the RAW body )
# Webhook subscriptions: public_key on the webhook object.
# Per-purchase success_callback: GET /public_key/
print(f"Received X-Signature: {signature}")
print(f"Raw payload length: {len(raw_body)}")
public_key = serialization.load_pem_public_key(public_key_pem.encode())
try:
public_key.verify(
base64.b64decode(signature),
raw_body, # raw bytes, never a re-serialised dict
padding.PKCS1v15(),
hashes.SHA256(),
)
print("Signature valid")
except Exception as exc:
print(f"Signature INVALID: {exc}")
```
### Issue: Webhook Timeout Errors [#issue-webhook-timeout-errors]
**Solution — Async Processing:**
```python
# Wrong - synchronous processing
@app.route('/webhook', methods=['POST'])
def webhook():
data = request.json
process_payment(data) # Slow - might timeout
return '', 200
# Correct - acknowledge immediately, process async
@app.route('/webhook', methods=['POST'])
def webhook():
if not verify_signature(request):
return '', 401
webhook_id = request.headers.get('X-Webhook-ID')
queue.enqueue(process_webhook, request.json, webhook_id)
return '', 200 # Return immediately
```
### Issue: Duplicate Webhooks [#issue-duplicate-webhooks]
```python
import redis
redis_client = redis.Redis()
def handle_webhook(data, webhook_id):
cache_key = f"webhook:{webhook_id}"
if redis_client.exists(cache_key):
return # Already processed
redis_client.setex(cache_key, 86400, "processing")
try:
process_event(data)
redis_client.setex(cache_key, 86400, "completed")
except Exception as e:
redis_client.delete(cache_key)
raise
```
### Issue: Testing Webhooks Locally [#issue-testing-webhooks-locally]
```bash
# Install and start ngrok
ngrok http 3000
# Use the generated URL in Precium dashboard
# e.g., https://abc123.ngrok.io/webhooks/precium
```
**Trigger a test webhook:**
```bash
curl -X POST https://gate.reviopay.com/api/v1/webhooks/test/ \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "purchase.paid",
"webhook_url": "https://abc123.ngrok.io/webhooks/precium"
}'
```
## Payment Decline Issues [#payment-decline-issues]
### Issue: "insufficient_funds" [#issue-insufficient_funds]
- Notify the customer of insufficient funds
- Suggest retry after funding the account
- Offer alternative payment methods
- For MIT: Schedule retry per retry logic
**Customer Message:***"Your payment couldn't be processed due to insufficient funds. Please add funds to your account and try again."*
### Issue: "do_not_honour" [#issue-do_not_honour]
**Potential Causes:**
- Card restrictions
- Unusual spending pattern
- Bank's fraud detection
- Account issues
**Actions:**
1. Suggest the customer contact their bank
2. Offer an alternative payment method
3. For MIT: Retry per schedule
### Issue: Token-Based Charge Fails [#issue-token-based-charge-fails]
**Common Causes:**
- Token expired
- Original card expired/replaced
- Wrong `previous_network_transaction_id`
- Incorrect `original_amount_cents`
**Verification Checklist:**
- Token is from the same client
- `previous_network_transaction_id` matches original CIT
- `original_amount_cents` matches tokenization amount
- `is_recurring: true` in purchase
### Issue: Refund Amount Exceeds Original [#issue-refund-amount-exceeds-original]
```python
original_amount = 29900
already_refunded = 5000
max_refundable = original_amount - already_refunded
if requested_refund > max_refundable:
raise ValueError(f"Can only refund up to {max_refundable}")
```
### Issue: Refund Pending for Days [#issue-refund-pending-for-days]
| Scenario | Timeline |
|---|---|
| Same-day (pre-settlement) | Cannot be voided — the refund runs to completion |
| Post-settlement | 5-10 business days |
| International cards | Up to 15 business days |
## Testing Issues [#testing-issues]
### Issue: Test Cards Not Working in Production [#issue-test-cards-not-working-in-production]
Test card numbers only work in the sandbox environment. Use real cards in production.
### Issue: Real Cards Not Working in Sandbox [#issue-real-cards-not-working-in-sandbox]
Sandbox doesn't process real cards. Use designated test card numbers for sandbox testing.
## Performance Issues [#performance-issues]
### Issue: API Requests Are Slow [#issue-api-requests-are-slow]
**Optimisation Checklist:**
1. Use connection pooling
2. Implement request timeouts
3. Cache client IDs (don't create new clients each time)
4. Use webhooks instead of polling for status
```python
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retry = Retry(total=3, backoff_factor=0.5)
adapter = HTTPAdapter(max_retries=retry, pool_connections=10, pool_maxsize=10)
session.mount('https://', adapter)
```
## When to Contact Support [#when-to-contact-support]
Contact Precium support (support@precium.com) when:
- URGENT priority errors persist
- Unexpected error codes not in documentation
- Settlement or reconciliation discrepancies
- Account configuration issues
- Suspected fraud or security concerns
**Include in Support Request:**
- Purchase ID(s)
- Timestamp of issue
- Full error response
- Steps to reproduce
- Expected vs actual behaviour
## Error Code Quick Lookup [#error-code-quick-lookup]
| Error | Action |
|---|---|
| `insufficient_funds` | Retry later, different card |
| `do_not_honour` | Retry later, contact bank |
| `expired_card` | New card required |
| `3ds_authentication_failed` | Retry authentication |
| `acquirer_configuration_error` | Contact support |
| `invalid_card_number` | Check card number format |
| `antifraud_general` | Do not retry, investigate |
| `authorization_expired` | Re-authorize |
| `capture_amount_exceeds_authorization` | Reduce capture amount |
| `void_not_permitted` | Check transaction status, use refund if settled |
| `partial_capture_not_supported` | Capture full amount or void and re-auth |
| `refund_amount_exceeds_original` | Check total refunded amount |
| `invalid_external_3ds_data` | Verify all required 3DS fields |
| `zero_auth_not_permitted` | Contact support to enable |
| `invalid_network_token` | Request new token from MDES/VTS |