# Authentication (/docs/direct-debit/authentication) The Precium API uses OAuth 2.0 Client Credentials Flow for authentication. ## Overview [#overview] All API requests must include a valid Bearer token in the Authorization header. Tokens are obtained by making a POST request to the authentication endpoint with your client credentials. ## Obtaining an Access Token [#obtaining-an-access-token] ### Endpoint [#endpoint] | Environment | URL | |---|---| | Sandbox | `https://dev-payce.auth.eu-west-1.amazoncognito.com/oauth2/token` | | Production | `https://prod-payce.auth.eu-west-1.amazoncognito.com/oauth2/token` | ### Request [#request] ```html POST /oauth2/token HTTP/1.1 Host: dev-payce.auth.eu-west-1.amazoncognito.com Content-Type: application/x-www-form-urlencoded Authorization: Basic BASE64_ENCODED_CREDENTIALS grant_type=client_credentials ``` ### cURL Example [#curl-example] ```bash curl -X POST https://dev-payce.auth.eu-west-1.amazoncognito.com/oauth2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -d "grant_type=client_credentials" ``` ### Parameters [#parameters] | Parameter | Type | Required | Description | |---|---|---|---| | `grant_type` | string | Yes | Must be `client_credentials` | ### Response [#response] ```json { "access_token": "eyJraWQiOiJYMHJhS...", "expires_in": 86400, "token_type": "Bearer" } ``` | Field | Type | Description | |---|---|---| | `access_token` | string | The Bearer token to use in API requests | | `expires_in` | integer | Token validity in seconds (86400 = 24 hours) | | `token_type` | string | Always `Bearer` | ## Using the Access Token [#using-the-access-token] Include the token in the `Authorization` header for all API requests: ```text GET /v2/merchantprofile/ HTTP/1.1 Host: dev.api.revio.co.za Authorization: Bearer eyJraWQiOiJYMHJhS... Content-Type: application/json ``` ### cURL Example [#curl-example-1] ```bash curl -X GET https://dev.api.revio.co.za/v2/merchantprofile/ \ -H "Authorization: Bearer eyJraWQiOiJYMHJhS..." \ -H "Content-Type: application/json" ``` ## Token Management Best Practices [#token-management-best-practices] ### Token Expiry [#token-expiry] Access tokens expire every **24 hours**. Implement token refresh logic in your application: ```jsx // Pseudocode example function getValidToken() { if (token && !isExpired(token)) { return token; } return refreshToken(); } ``` ### Security Guidelines [#security-guidelines] 1. **Store tokens securely**: Access tokens should be stored in an encrypted format at rest if stored server-side 2. **Limit access**: The number of employees with access to tokens should be kept to a need-to-know basis 3. **Report breaches immediately**: If a token breach is detected, contact Precium immediately so affected tokens can be revoked 4. **Never expose in client-side code**: Do not include credentials or tokens in client-side JavaScript or mobile apps 5. **Use HTTPS**: All API requests must be made over HTTPS ### Error Handling [#error-handling] If your token is invalid or expired, the API will return: ```json { "statusCode": 401, "message": "Unauthorized" } ``` When you receive a 401 response, request a new access token before retrying the request. ## Test Credentials [#test-credentials] To request sandbox credentials, contact the Precium support team at support@precium.com Your sandbox credentials will include: - `client_id` (username) - `client_secret` (password) ## Production Credentials [#production-credentials] The production base URL is `https://api.revio.co.za/`. Your production `client_id` and `client_secret` are issued at go-live. Contact your Precium account manager to initiate the onboarding process. ## API Endpoints [#api-endpoints] After authentication, use the following base URLs: | Environment | Base URL | |---|---| | Sandbox | `https://dev.api.revio.co.za/` | | Production | `https://api.revio.co.za/` | ## Troubleshooting [#troubleshooting] ### Common Issues [#common-issues] | Error | Cause | Solution | |---|---|---| | `401 Unauthorized` | Invalid or expired token | Request a new access token | | `403 Forbidden` | Insufficient permissions | Contact support to verify account setup | | `invalid_client` | Incorrect credentials | Verify your client_id and client_secret | | `unsupported_grant_type` | Wrong grant_type value | Ensure `grant_type=client_credentials` | ### Testing Your Token [#testing-your-token] Verify your token is working by calling the merchant profile endpoint: ```bash curl -X GET https://dev.api.revio.co.za/v2/merchantprofile/ \ -H "Authorization: Bearer YOUR_TOKEN" ``` A successful response confirms your authentication is configured correctly. # DebiCheck (/docs/direct-debit/debicheck) DebiCheck is an authenticated debit order system that requires customers to electronically confirm mandate details before collections can be processed. ## Overview [#overview] DebiCheck provides: - **Reduced disputability**: Authenticated mandates are harder to dispute - **Higher success rates**: Early processing window priority - **Account tracking**: Monitor accounts for up to 10 days post-action date - **Electronic mandate storage**: Banks store authenticated mandates ## How DebiCheck Works [#how-debicheck-works] 1. **Mandate Creation**: Submit the mandate request with the customer details 2. **Authentication**: Customer receives and approves the mandate via their bank 3. **Confirmation**: Mandate status changes to `COMPLETED` 4. **Collection**: Create collections against authenticated mandates ## Authentication Types [#authentication-types] | Type | Code | Description | Response Time | |---|---|---|---| | Real-Time | TT1 | USSD push notification | 120 seconds | | Delayed Real-Time | TT1b | Extended approval window | Same day until 20:00 | | Batch | TT2 | Overnight processing | Up to 48 hours | ### Real-Time (TT1) [#real-time-tt1] Customer receives an immediate USSD/push notification and must approve within 120 seconds. ```json { "authenticationType": "REALTIME", "realtimeAuthRetries": 0 } ``` ### Delayed Real-Time (TT1b) [#delayed-real-time-tt1b] Mandate is lodged within 60 seconds, the customer has until 20:00 the same day to approve. ```json { "authenticationType": "DELAYED" } ``` ### Batch (TT2) [#batch-tt2] Mandate submitted overnight, the customer has until 19:00 the following day to approve. ```json { "authenticationType": "BATCH" } ``` ## Mandates [#mandates] ### Create DebiCheck Mandate [#create-debicheck-mandate] ### Endpoint [#endpoint] ```text POST https://api.revio.co.za/v2/mandate/debicheck/ ``` ### Request (Fixed Value) [#request-fixed-value] ```bash curl -X POST https://dev.api.revio.co.za/v2/mandate/debicheck/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "initiationDate": "2024-01-15T10:00:00.000Z", "valueType": "FIXED", "debitSequence": "RCUR", "maxAmountCents": 10000, "amountCents": 10000, "allowDateAdjustment": true, "adjustmentCategory": "NEVER", "authenticationType": "REALTIME", "realtimeAuthRetries": 0, "fallbackAuthenticationType": "DELAYED_WITHOUT_AUTH", "profileCode": "TEST1", "abbreviatedName": "TESTMERCH1", "contractReference": "DC001", "debtor": { "accountNumber": "010553922", "accountType": "CURRENT", "bank": "ABSA", "branchCode": "632500", "firstName": "John", "lastName": "Smith", "identification": { "emailAddress": "john@example.com", "idNumber": "0001010000006", "phoneNumber": "+27-123456789", "passportNumber": "" } }, "creditor": { "accountNumber": "12345678", "accountType": "CURRENT", "branchCode": "051001", "name": "TEST MERCHANT 1", "bank": "STANDARDBANK", "phoneNumber": "+27-615333440", "emailAddress": "merchant@test.com" }, "releaseDate": "2024-12-31T00:00:00.000Z", "frequency": "MONTHLY", "collectionDay": 20, "currency": "ZAR", "entryClass": "21", "verifyAccount": false }' ``` ### Request Fields [#request-fields] | Field | Type | Required | Description | |---|---|---|---| | `initiationDate` | string | Yes | Date mandate is initiated | | `valueType` | string | Yes | Value type: `FIXED`, `VARIABLE`, `USAGEBASED` | | `debitSequence` | string | Yes | Sequence type: `RCUR`, `OOFF`, `FRST`, `FNAL` | | `maxAmountCents` | integer | Yes | Maximum collection amount in cents | | `amountCents` | integer | Yes | Standard collection amount in cents | | `allowDateAdjustment` | boolean | Yes | Allow collection date variation | | `adjustmentCategory` | string | Yes | Adjustment frequency | | `authenticationType` | string | Yes | Auth type: `REALTIME`, `DELAYED`, `BATCH` | | `realtimeAuthRetries` | integer | No | Number of real-time retry attempts | | `fallbackAuthenticationType` | string | No | Fallback if auth fails | | `profileCode` | string | Yes | Your merchant profile code | | `abbreviatedName` | string | Yes | Your PASA-registered name | | `contractReference` | string | Yes | Contract reference (max 14 chars) | | `debtor` | object | Yes | Customer details | | `creditor` | object | Yes | Merchant details | | `ultimateCreditor` | object | No | Split payment recipient | | `releaseDate` | string | No | Mandate release date | | `firstCollectionDate` | string | No | First collection date (if different) | | `firstCollectionAmountCents` | integer | No | First collection amount (if different) | | `frequency` | string | Yes | Collection frequency | | `collectionDay` | integer | Yes | Day of month (1-31) | | `trackingPeriod` | integer | No | Days to track account (0-10) | | `currency` | string | Yes | Currency (`ZAR`) | | `entryClass` | string | Yes | Always `21` | | `verifyAccount` | boolean | No | Perform account verification | ### Value Types [#value-types] | Type | Description | Max Amount Limit | |---|---|---| | `FIXED` | No amount adjustments allowed | Equal to `amountCents` | | `VARIABLE` | Amount adjustments allowed, disputable | 1.5x `amountCents` | | `USAGEBASED` | Full flexibility, disputable | No specific limit | ### Debit Sequence Types [#debit-sequence-types] | Type | Description | |---|---| | `FRST` | First collection of mandate | | `OOFF` | Once-off (single collection) | | `RCUR` | Recurring collection | | `RPRE` | Re-presented after failure | | `FNAL` | Final collection of mandate | ### Adjustment Categories [#adjustment-categories] | Value | Description | |---|---| | `NEVER` | No adjustments allowed | | `QUARTERLY` | Adjust every 3 months | | `BIANNUALLY` | Adjust every 6 months | | `ANNUALLY` | Adjust every year | ### Fallback Authentication Types [#fallback-authentication-types] Please note that fallback to `DELAYED_WITHOUT_AUTH` was deprecated in 2025 in line with PASA requirements. | Value | Description | |---|---| | `DELAYED` | Fall back to TT2 batch | | `DELAYED_WITHOUT_AUTH` | Fall back without re-authentication | ### Response [#response] ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "PENDING_AUTHENTICATION", "authenticationType": "REALTIME", "valueType": "FIXED", "contractReference": "DC001", "debtor": { ... }, "creditor": { ... } } ``` ### Variable Value Mandate [#variable-value-mandate] ```json { "valueType": "VARIABLE", "adjustmentCategory": "QUARTERLY", "adjustmentRate": "10.0", "maxAmountCents": 15000, "amountCents": 10000 } ``` Note: Use either adjustmentRate (percentage) OR adjustmentAmountCents, not both. ### Usage-Based Mandate [#usage-based-mandate] ```json { "valueType": "USAGEBASED", "adjustmentCategory": "NEVER", "maxAmountCents": 50000, "amountCents": 10000 } ``` ### Get Mandate Status [#get-mandate-status] ### Endpoint [#endpoint-1] ```text GET https://api.revio.co.za/v2/mandate/debicheck/{mandateId}/status/ ``` ### Request [#request] ```bash curl -X GET https://dev.api.revio.co.za/v2/mandate/debicheck/a1b2c3d4-e5f6-7890-abcd-ef1234567890/status/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response [#response-1] ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "COMPLETED" } ``` ### Mandate Statuses [#mandate-statuses] | Status | Description | |---|---| | `NEW` | Mandate created, not yet submitted | | `PENDING_AUTHENTICATION` | Waiting for customer approval | | `COMPLETED` | Customer approved mandate | | `AUTH_FAILURE` | Authentication failed or rejected | | `CANCELLED` | Mandate cancelled | | `SUSPENDED` | Mandate temporarily suspended | ### Get Mandate Details [#get-mandate-details] ### Endpoint [#endpoint-2] ```text GET https://api.revio.co.za/v2/mandate/debicheck/{mandateId} ``` ### Response [#response-2] ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "state": "COMPLETED", "referenceNumber": "REF123456", "authenticationReference": "AUTH789", "lastSuccessfulAuditId": "AUDIT456", "mandateReferenceNumber": "MRN001", ... } ``` Store these values for mandate amendments: - `referenceNumber` - `authenticationReference` - `lastSuccessfulAuditId` ### Amend DebiCheck Mandate [#amend-debicheck-mandate] Modify an existing authenticated mandate. ### Endpoint [#endpoint-3] ```text PUT https://api.revio.co.za/v2/mandate/debicheck/{mandateId} ``` ### Request [#request-1] ```bash curl -X PUT https://dev.api.revio.co.za/v2/mandate/debicheck/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "state": "COMPLETED", "referenceNumber": "REF123456", "authenticationReference": "AUTH789", "lastSuccessfulAuditId": "AUDIT456", "maxAmountCents": 15000, ... }' ``` ### Amendment Outcomes [#amendment-outcomes] | Change | Result | |---|---| | ID Number, Names, Phone, Email | No re-authentication | | Tracking, First Collection, Abbreviated Name | Re-authentication required | | Collection Day, Contract Reference | Re-authentication required | | Debit Amounts (if outside bounds) | Re-authentication required | | Debtor Account Number | New mandate required (AUTH_FAILURE) | | Creditor Details | New mandate required (AUTH_FAILURE) | ## Collections [#collections] ### Create DebiCheck Collection [#create-debicheck-collection] ### Endpoint [#endpoint-4] ```text POST https://api.revio.co.za/v2/collection/debicheck/from_mandate ``` ### Request [#request-2] ```bash curl -X POST https://dev.api.revio.co.za/v2/collection/debicheck/from_mandate \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mandateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "collectionDate": "2024-02-20T00:00:00.000Z", "cycleDate": "2024-02-20T00:00:00.000Z", "debitSequence": "RCUR", "amountCents": 10000, "trackingPeriod": 0 }' ``` ### Request Fields [#request-fields-1] | Field | Type | Required | Description | |---|---|---|---| | `mandateId` | string | Yes | Authenticated mandate ID | | `collectionDate` | string | Yes | Collection date | | `cycleDate` | string | Yes | Billing cycle date | | `debitSequence` | string | Yes | Sequence type | | `amountCents` | integer | Yes | Amount in cents | | `trackingPeriod` | integer | No | Override mandate tracking days (0-10) | Note: Use FRST for first/pro-rata collections, RCUR for recurring. ### Response [#response-3] ```json { "id": "coll-123-456-789", "mandateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "PENDING", "collectionDate": "2024-02-20T00:00:00.000Z", "amountCents": 10000 } ``` ### Get Collection Status [#get-collection-status] ### Endpoint [#endpoint-5] ```text GET https://api.revio.co.za/v2/collection/debicheck/{collectionId}/status ``` ## Cut-off Times [#cut-off-times] | Service | Weekday | Weekend | |---|---|---| | DebiCheck/RMS Collect | 08:00 - 16:00 (2 days before) | 08:00 - 11:00 (2 days before) | | DebiCheck TT1a/b | 03:00 - 18:30 | 03:00 - 18:30 | | DebiCheck TT2 | 03:00 - 18:30 | 03:00 - 18:30 | Important: DebiCheck mandates can only be obtained between 06:00 and 20:00 on weekdays. ## Account Tracking [#account-tracking] DebiCheck supports account tracking for up to 10 days post-action date: ```json { "trackingPeriod": 10 } ``` If a collection fails due to insufficient funds, the bank will monitor the account and retry when funds are available. ## Best Practices [#best-practices] 1. **Have a timing plan**: Switch to TT2 for authentication outside business hours 2. **Handle failures**: Implement a fallback to `DELAYED_WITHOUT_AUTH` 3. **Plan for ignores**: Follow up with customers who don't respond to authentication 4. **Use correct value type**: Choose `FIXED`, `VARIABLE`, or `USAGEBASED` based on your use case 5. **Set appropriate max amounts**: Ensure `maxAmountCents` covers your needs 6. **Submit early**: Present collections at least 2 calendar days before collection day 7. **Match collection days**: Present collections for the authenticated collection day ## Testing DebiCheck Mandates [#testing-debicheck-mandates] Use these contract references to simulate scenarios: | Contract Reference | Scenario | |---|---| | `NORESPONSE` | Customer doesn't respond within timeout | | `FAILINIT` | Bank error during authentication | | `REJECT` | Customer rejects mandate | | `TIMEOUT` | Authentication times out | See Testing for more test scenarios. # EFT Debit orders (/docs/direct-debit/eft-debit-orders) EFT debit orders (standard debit orders) enable merchants to collect funds from customer bank accounts with written, electronic, or voice-recorded authorisation. ## Overview [#overview] EFT debit orders are cost-efficient payment instruments for collecting recurring revenue. The process involves: 1. **Create a Mandate**: Obtain and store customer authorisation 2. **Create Collections**: Submit collection instructions against the mandate 3. **Monitor Status**: Track collection results and handle failures ## Service Types [#service-types] | Service Type | Description | Settlement | |---|---|---| | `SAMEDAY` | Processed and settled same day | Same day | | `ONEDAY` | Processed within one business day | Next business day | | `TWODAY` | Processed within two business days | Two business days | ## Mandates [#mandates] A mandate represents the customer's authorisation to debit their account. ### Create EFT Mandate [#create-eft-mandate] ### Endpoint [#endpoint] ```text POST https://api.revio.co.za/v2/mandate/eft/ ``` ### Request [#request] ```bash curl -X POST https://dev.api.revio.co.za/v2/mandate/eft/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "defaultServiceType": "ONEDAY", "profileCode": "TEST1", "abbreviatedName": "TESTMERCH1", "contractReference": "CONTRACT001", "debtor": { "accountNumber": "010553922", "accountType": "CURRENT", "bank": "ABSA", "branchCode": "632005", "firstName": "John", "lastName": "Smith", "identification": { "emailAddress": "john@example.com", "idNumber": "0001010000006", "phoneNumber": "+27-123456789", "passportNumber": "" } }, "creditor": { "accountNumber": "12345678", "accountType": "CURRENT", "branchCode": "051001", "name": "TEST MERCHANT 1", "bank": "STANDARDBANK", "phoneNumber": "+27-615333440", "emailAddress": "merchant@test.com" }, "releaseDate": "2024-12-31T00:00:00.000Z", "frequency": "MONTHLY", "collectionDay": 15, "amountCents": 10000, "currency": "ZAR", "entryClass": "21", "verifyAccount": false }' ``` ### Request Fields [#request-fields] | Field | Type | Required | Description | |---|---|---|---| | `defaultServiceType` | string | Yes | Default service type: `SAMEDAY`, `ONEDAY`, `TWODAY` | | `profileCode` | string | Yes | Your merchant profile code | | `abbreviatedName` | string | Yes | Your PASA-registered abbreviated name | | `contractReference` | string | Yes | Unique contract reference (max 14 characters) | | `externalReference` | string | No | Your external reference ID (must be unique) | | `debtor` | object | Yes | Customer (payer) details | | `creditor` | object | Yes | Merchant (payee) details | | `ultimateCreditor` | object | No | Split payment recipient (if required) | | `releaseDate` | string | No | Date when mandate becomes active | | `frequency` | string | Yes | Collection frequency | | `collectionDay` | integer | Yes | Day of month for collections (1-31) | | `amountCents` | integer | Yes | Collection amount in cents | | `currency` | string | Yes | Currency code (`ZAR`) | | `entryClass` | string | Yes | Always `21` | | `verifyAccount` | boolean | No | Perform bank account verification | ### Debtor Object [#debtor-object] | Field | Type | Required | Description | |---|---|---|---| | `accountNumber` | string | Yes | Customer bank account number | | `accountType` | string | Yes | Account type: `CURRENT`, `SAVINGS`, `TRANSMISSION` | | `bank` | string | Yes | Bank name (uppercase, one word) | | `branchCode` | string | Yes | Bank branch code | | `firstName` | string | Yes | Customer first name | | `lastName` | string | Yes | Customer last name | | `identification.emailAddress` | string | Yes | Customer email | | `identification.idNumber` | string | Yes | SA ID number | | `identification.phoneNumber` | string | Yes | Phone number (format: `+27-XXXXXXXXX`) | | `identification.passportNumber` | string | No | Passport number (if no ID) | ### Creditor Object [#creditor-object] | Field | Type | Required | Description | |---|---|---|---| | `accountNumber` | string | Yes | Your settlement account number | | `accountType` | string | Yes | Account type | | `branchCode` | string | Yes | Your bank branch code | | `name` | string | Yes | Your registered merchant name | | `bank` | string | Yes | Your bank name | | `phoneNumber` | string | Yes | Your contact number | | `emailAddress` | string | Yes | Your email address | ### Frequency Values [#frequency-values] | Value | Description | |---|---| | `WEEKLY` | Every week | | `MONTHLY` | Every month | | `QUARTERLY` | Every 3 months | | `BIANNUALLY` | Every 6 months | | `ANNUALLY` | Every year | ### Bank Names [#bank-names] Use uppercase, single-word format: - `ABSA` - `STANDARDBANK` - `FNB` - `NEDBANK` - `CAPITEC` - `INVESTEC` - `BIDVEST` ### Response [#response] ```json { "id": "8f4a6b0f-fa9b-4f45-8e19-b75c1a70432a", "status": "ACTIVE", "defaultServiceType": "ONEDAY", "profileCode": "TEST1", "abbreviatedName": "TESTMERCH1", "contractReference": "CONTRACT001", "debtor": { ... }, "creditor": { ... }, "frequency": "MONTHLY", "collectionDay": 15, "amountCents": 10000, "createdAt": "2024-01-15T10:30:00.000Z" } ``` ### Bank Account Verification [#bank-account-verification] Set `verifyAccount: true` to verify the debtor's bank account before creating the mandate: ```json { "verifyAccount": true } ``` The verification checks: - Account exists - Account is open - Account type matches - Account accepts debits - ID number matches the account holder If verification fails, the mandate will not be created. See Bank Account Verification for details. ### Ultimate Creditor (Split Payments) [#ultimate-creditor-split-payments] For split payment scenarios, include the `ultimateCreditor` object: ```json { "ultimateCreditor": { "accountNumber": "010553983", "accountType": "CURRENT", "branchCode": "051001", "name": "PARTNER COMPANY", "bank": "STANDARDBANK", "phoneNumber": "+27-615333440", "emailAddress": "partner@test.com", "idNumber": "0001010000089" } } ``` ## Collections [#collections] Collections are individual debit instructions processed against a mandate. ### Create EFT Collection [#create-eft-collection] ### Endpoint [#endpoint-1] ```text POST https://api.revio.co.za/v2/collection/eft/from_mandate/ ``` ### Request [#request-1] ```bash curl -X POST https://dev.api.revio.co.za/v2/collection/eft/from_mandate/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mandateId": "8f4a6b0f-fa9b-4f45-8e19-b75c1a70432a", "collectionDate": "2024-02-15T00:00:00.000Z", "amountCents": 10000, "serviceType": "ONEDAY" }' ``` ### Request Fields [#request-fields-1] | Field | Type | Required | Description | |---|---|---|---| | `mandateId` | string | Yes | ID of the mandate to collect against | | `collectionDate` | string | Yes | Date to process collection (ISO 8601) | | `amountCents` | integer | Yes | Collection amount in cents | | `externalReference` | string | No | Your external reference ID | | `serviceType` | string | No | Override default service type | ### Response [#response-1] ```json { "id": "63d94be3-1faf-428b-89f3-bc5271be8015", "mandateId": "8f4a6b0f-fa9b-4f45-8e19-b75c1a70432a", "status": "PENDING", "collectionDate": "2024-02-15T00:00:00.000Z", "amountCents": 10000, "serviceType": "ONEDAY" } ``` ### Get Collection Status [#get-collection-status] ### Endpoint [#endpoint-2] ```text GET https://api.revio.co.za/v2/collection/eft/{collectionId}/status/ ``` ### Request [#request-2] ```bash curl -X GET https://dev.api.revio.co.za/v2/collection/eft/63d94be3-1faf-428b-89f3-bc5271be8015/status/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response [#response-2] ```json { "id": "63d94be3-1faf-428b-89f3-bc5271be8015", "status": "COMPLETED", "statementId": "stmt_12345" } ``` ### Collection Statuses [#collection-statuses] | Status | Description | |---|---| | `PENDING` | Collection submitted, awaiting processing | | `PROCESSING` | Collection being processed by bank | | `COMPLETED` | Collection successful | | `UNPAID` | Collection failed (insufficient funds, etc.) | | `DISPUTED` | Customer disputed the collection | | `CANCELLED` | Collection cancelled before processing | ## Bank Account Verification [#bank-account-verification-1] Verify customer bank details before creating mandates. ### Verification Checks [#verification-checks] | Check | Field | Description | |---|---|---| | `AccountExists` | Account number | Account exists at bank | | `AccountIsOpen` | Account status | Account is active/open | | `AccountTypeMatches` | Account type | Provided type matches actual | | `AccountAcceptsDebits` | Account capabilities | Account can receive debits | | `IdNumberMatches` | ID number | ID matches account holder | ### Failure Response [#failure-response] ```json { "value": { "Message": "Debtor account verification failure: details={\"AccountExists\":true,\"AccountIsOpen\":false,\"AccountTypeMatches\":null,\"IdNumberMatches\":null,\"AccountAcceptsDebits\":null}" }, "statusCode": 400 } ``` ## Cut-off Times [#cut-off-times] Submit collections before these cut-off times: | Service Type | Weekday Cut-off | Weekend Cut-off | |---|---|---| | Same-day | 08:00 - 16:20 | 08:00 - 09:45 | | One-day (dated) | 08:00 - 16:00 | 08:00 - 10:15 | | Two-day (dated) | 08:00 - 14:30 (2 days before) | 08:00 - 09:00 | ## Reconciliation [#reconciliation] Revio automatically reconciles collection results to bank statements. Successfully reconciled collections include a `statementId` field: ```json { "id": "63d94be3-1faf-428b-89f3-bc5271be8015", "status": "COMPLETED", "statementId": "stmt_12345" } ``` Contact your Revio account manager to enable automatic reconciliation. ## Error Handling [#error-handling] ### Common Errors [#common-errors] | Error | Cause | Resolution | |---|---|---| | `400 Bad Request` | Invalid request data | Check required fields and formats | | `404 Not Found` | Mandate/collection not found | Verify the ID | | `409 Conflict` | Duplicate external reference | Use a unique reference | ### Unpaid Collections [#unpaid-collections] When a collection returns unpaid: - Do not resubmit until the next mandated date - After 2 consecutive "Not provided for" failures, obtain a new mandate - "Account Closed" or "Payment Stopped" requires customer contact ## Best Practices [#best-practices] 1. **Store mandate IDs**: Keep track of mandate IDs for future collections 2. **Use external references**: Add your own references for easier reconciliation 3. **Handle failures gracefully**: Implement retry logic respecting PASA rules 4. **Verify accounts**: Use `verifyAccount: true` to reduce failed collections 5. **Submit early**: Submit collections well before cut-off times # EFT Payments (/docs/direct-debit/eft-payments) EFT Payments (EFT Credits) enable merchants to send outbound payments to bank accounts. ## Overview [#overview] The EFT Credit service handles financial transactions, including: - Salary disbursements - Account payments - Refunds - Supplier payments ## Service Types [#service-types] | Service Type | Description | Settlement | |---|---|---| | `SAMEDAY` | Same-day value (SDV) | Same day | | `ONEDAY` | One business day | Next business day | | `TWODAY` | Two business days | Two business days | ## Service Limitations [#service-limitations] - **Maximum transaction**: R5,000,000 (5 million ZAR) - Transactions ≥ R5m will be returned with "exceeds industry imposed limits" - High-value transactions should use real-time line processing ## Supported Account Types [#supported-account-types] - Current accounts - Savings accounts - Transmission accounts ## Create EFT Payment [#create-eft-payment] ### Endpoint [#endpoint] ```text POST https://api.revio.co.za/v2/payment/eft ``` ### Request (Same-day) [#request-same-day] ```bash curl -X POST https://dev.api.revio.co.za/v2/payment/eft \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "paymentDate": "2024-01-15T08:00:00.000Z", "profileCode": "TEST1", "abbreviatedName": "TESTMERCH1", "contractReference": "PAY001", "amountCents": 50000, "serviceType": "SAMEDAY", "debtor": { "accountNumber": "12345678", "branchCode": "051001", "accountType": "CURRENT", "bank": "STANDARDBANK", "firstName": "TEST MERCHANT 1", "identification": { "phoneNumber": "+27-615333440", "emailAddress": "merchant@test.com" } }, "creditor": { "accountNumber": "010553922", "accountType": "CURRENT", "branchCode": "632005", "name": "John Smith", "idNumber": "0001010000006", "phoneNumber": "+27-123456789" } }' ``` ### Request Fields [#request-fields] | Field | Type | Required | Description | |---|---|---|---| | `paymentDate` | string | Yes | Date to process payment (ISO 8601) | | `profileCode` | string | Yes | Your merchant profile code | | `abbreviatedName` | string | Yes | Your PASA-registered name | | `contractReference` | string | Yes | Payment reference | | `amountCents` | integer | Yes | Payment amount in cents | | `serviceType` | string | Yes | `SAMEDAY`, `ONEDAY`, or `TWODAY` | | `debtor` | object | Yes | Source account (your account) | | `creditor` | object | Yes | Destination account (recipient) | ### Debtor Object (Source Account) [#debtor-object-source-account] | Field | Type | Required | Description | |---|---|---|---| | `accountNumber` | string | Yes | Your bank account number | | `branchCode` | string | Yes | Your bank branch code | | `accountType` | string | Yes | Account type | | `bank` | string | Yes | Your bank name | | `firstName` | string | Yes | Your merchant name | | `identification.phoneNumber` | string | Yes | Your contact number | | `identification.emailAddress` | string | Yes | Your email address | ### Creditor Object (Destination Account) [#creditor-object-destination-account] | Field | Type | Required | Description | |---|---|---|---| | `accountNumber` | string | Yes | Recipient account number | | `accountType` | string | Yes | Account type | | `branchCode` | string | Yes | Recipient branch code | | `name` | string | Yes | Recipient name | | `idNumber` | string | Yes | Recipient ID number | | `phoneNumber` | string | Yes | Recipient phone number | ### Response [#response] ```json { "id": "pay-123-456-789", "status": "PENDING", "paymentDate": "2024-01-15T08:00:00.000Z", "amountCents": 50000, "serviceType": "SAMEDAY", "contractReference": "PAY001" } ``` ## Payment Examples [#payment-examples] ### One-day Payment [#one-day-payment] ```bash curl -X POST https://dev.api.revio.co.za/v2/payment/eft \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "paymentDate": "2024-01-15T08:00:00.000Z", "profileCode": "TEST1", "abbreviatedName": "TESTMERCH1", "contractReference": "PAY002", "amountCents": 100000, "serviceType": "ONEDAY", "debtor": { "accountNumber": "12345678", "branchCode": "051001", "accountType": "CURRENT", "bank": "STANDARDBANK", "firstName": "TEST MERCHANT 1", "identification": { "phoneNumber": "+27-615333440", "emailAddress": "merchant@test.com" } }, "creditor": { "accountNumber": "987654321", "accountType": "SAVINGS", "branchCode": "250655", "name": "Jane Doe", "idNumber": "8501015800089", "phoneNumber": "+27-987654321" } }' ``` ### Two-day Payment [#two-day-payment] ```json { "serviceType": "TWODAY", ... } ``` ## Get Payment Status [#get-payment-status] ### Endpoint [#endpoint-1] ```text GET https://api.revio.co.za/v2/payment/eft/{paymentId}/status ``` ### Request [#request] ```bash curl -X GET https://dev.api.revio.co.za/v2/payment/eft/pay-123-456-789/status \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ### Response [#response-1] ```json { "id": "pay-123-456-789", "status": "COMPLETED" } ``` ### Payment Statuses [#payment-statuses] | Status | Description | |---|---| | `PENDING` | Payment submitted, awaiting processing | | `PROCESSING` | Payment being processed by bank | | `COMPLETED` | Payment successful | | `FAILED` | Payment failed | | `CANCELLED` | Payment cancelled | ## Cut-off Times [#cut-off-times] | Service Type | Weekday Cut-off | Weekend Cut-off | |---|---|---| | Same-day (SSVS) | 08:00 - 16:20 | 08:00 - 09:45 | | Two-day | 08:00 - 14:30 (2 days before) | 08:00 - 09:00 | Important: Submit payments before cut-off times to ensure same-day processing. Payments submitted after cut-off will automatically be processed in the next available processing day ## Error Handling [#error-handling] ### Common Errors [#common-errors] | Error | Cause | Resolution | |---|---|---| | `400 Bad Request` | Invalid request data | Verify all required fields | | `exceeds industry imposed limits` | Amount ≥ R5m | Use real-time line for large transactions | | `Invalid account` | Invalid creditor details | Verify account number and branch code | ### Failed Payments [#failed-payments] If a payment fails, check: 1. Creditor account details are correct 2. Your account has sufficient funds 3. Payment was submitted before the cut-off ## Best Practices [#best-practices] 1. **Verify recipient details**: Confirm account numbers before processing 2. **Use appropriate service type**: Choose based on urgency and cost 3. **Submit early**: Process payments well before cut-off times 4. **Track payments**: Monitor status and handle failures promptly 5. **Keep records**: Store payment IDs and references for reconciliation ## Bulk Payments [#bulk-payments] For processing multiple payments, submit separate requests for each. Consider implementing: - Batching logic in your application - Rate limiting to avoid overwhelming the API - Error handling for partial batch failures Contact Precium support for high-volume payment requirements. # Glossary (/docs/direct-debit/glossary) ## A [#a] ### Abbreviated Name [#abbreviated-name] A short identifier (maximum 10 characters) that appears on customer bank statements to identify the merchant. Must be registered and approved by PASA. Example: `TESTMERCH1` ### Account Holder [#account-holder] The person or entity that owns a bank account. For debit orders, this is typically the customer (debtor) whose account will be debited. ### Account Type [#account-type] The classification of a bank account. Supported types: - `CURRENT` - Cheque/current account - `SAVINGS` - Savings account - `TRANSMISSION` - Transmission account ### Action Date [#action-date] The date on which a payment instruction is processed. The smallest unit is a day. Collections submitted after cut-off times will be processed on the next action date. ### Acquiring Bank [#acquiring-bank] See Sponsoring Bank. ### Adjustment Category [#adjustment-category] For DebiCheck mandates, defines how often the collection amount can be adjusted: - `NEVER` - No adjustments allowed - `QUARTERLY` - Every 3 months - `BIANNUALLY` - Every 6 months - `ANNUALLY` - Every year ### Adjustment Rate [#adjustment-rate] A percentage by which the authenticated DebiCheck amount can increase, depending on the adjustment category. Cannot be used together with Adjustment Amount. ### Authentication Reference [#authentication-reference] A unique identifier assigned to a DebiCheck mandate after successful authentication. Required for mandate amendments. ### Authentication Type [#authentication-type] The method used to authenticate a DebiCheck mandate: - `REALTIME` (TT1) - 120-second USSD/push response - `DELAYED` (TT1b) - Same-day approval until 20:00 - `BATCH` (TT2) - 48-hour approval window ### AVS-R (Account Verification Service - Real-time) [#avs-r-account-verification-service---real-time] A service that validates customer bank account details, including account existence, status, type, and ID number matching. ## B [#b] ### Bankserv [#bankserv] The South African automated clearing house that processes electronic payment instructions between banks. ### Batch Processing [#batch-processing] Processing multiple transactions together at scheduled intervals rather than in real-time. DebiCheck TT2 mandates use batch processing. ### Bearer Token [#bearer-token] An access token used for API authentication. Included in the Authorization header of API requests. ### Branch Code [#branch-code] A numeric code identifying a specific bank branch. Used in conjunction with account numbers for payment routing. ## C [#c] ### Callback [#callback] An HTTP request sent by Precium to a merchant's server when a transaction status changes. Also known as a webhook. ### Callback Type [#callback-type] The category of events for which callbacks are sent: - `EFT_COLLECTION` - `DEBICHECK_COLLECTION` - `DEBICHECK_MANDATE` - `EFT_PAYMENT` ### Clearing Participant [#clearing-participant] A bank that participates in the national payment system and can process clearing transactions. ### Client Credentials Flow [#client-credentials-flow] An OAuth 2.0 authentication flow where the client authenticates using a client ID and secret to obtain an access token. ### Collection [#collection] A debit order transaction that withdraws funds from a customer's (debtor's) account to the merchant's (creditor's) account. ### Collection Day [#collection-day] The day of the month (1-31) on which recurring collections are processed. ### Contract Reference [#contract-reference] A unique identifier (maximum 14 characters) linking a mandate to a specific customer contract. Appears on bank statements alongside the abbreviated name. ### Creditor [#creditor] The party receiving funds in a transaction. In debit orders, this is the merchant. In payments, this is the recipient. ### Cut-off Time [#cut-off-time] The deadline by which transactions must be submitted to be processed on a specific action date. ## D [#d] ### DebiCheck [#debicheck] An authenticated debit order system where customers electronically approve mandate details before collections can be processed. Provides reduced disputability and higher success rates. ### Debit Order [#debit-order] An instruction authorising a third party (merchant) to collect funds from a customer's bank account on specified dates. ### Debit Sequence [#debit-sequence] Indicates the position of a collection within a mandate's lifecycle: - `FRST` - First collection - `OOFF` - Once-off (single collection) - `RCUR` - Recurring collection - `RPRE` - Re-presented after failure - `FNAL` - Final collection ### Debtor [#debtor] The party from whom funds are collected. In debit orders, this is the customer whose account is debited. ### Delayed Authentication [#delayed-authentication] DebiCheck authentication (TT1b) where the customer has until 20:00 on the same day to approve, rather than the 120-second real-time window. ### Dispute [#dispute] A challenge raised by a customer against a debit order transaction, claiming it was unauthorised or incorrect. ## E [#e] ### EFT (Electronic Funds Transfer) [#eft-electronic-funds-transfer] Electronic transfer of funds between bank accounts. Includes both debits (collections) and credits (payments). ### EFT Credit [#eft-credit] An outbound payment from the merchant's account to another account. Also called EFT Payment. ### EFT Debit [#eft-debit] An inbound collection from a customer's account to the merchant's account. Also called standard debit order. ### Entry Class [#entry-class] A classification code for debit order transactions. Always `21` for Precium transactions. ### External Reference [#external-reference] An optional merchant-provided identifier for tracking transactions in external systems. Must be unique across all transactions. ## F [#f] ### Fallback Authentication [#fallback-authentication] An alternative authentication method used when the primary DebiCheck authentication fails. Example: `DELAYED_WITHOUT_AUTH` ### Frequency [#frequency] How often collections are processed against a mandate: - `WEEKLY` - `MONTHLY` - `QUARTERLY` - `BIANNUALLY` - `ANNUALLY` ## H [#h] ### Homing Fee [#homing-fee] A fee charged by the paying (issuing) bank to the acquiring bank for processing a debit order against their customer's account. ## I [#i] ### ID Number [#id-number] South African identity number used to identify account holders. Format: 13 digits (YYMMDD SSSS C A Z). ### Input File [#input-file] A file containing payment/collection instructions uploaded to the bank for processing. ### Instrument [#instrument] The payment method or channel used for a transaction: - `EFTSAMEDAY` - `EFTONEDAY` - `EFTTWODAY` - `DEBICHECK` ### Issuing Bank [#issuing-bank] See Paying Bank. ## M [#m] ### Mandate [#mandate] A formal authorisation from a customer (debtor) allowing a merchant (creditor) to debit their bank account. Can be paper, electronic, or voice-recorded. ### Mandate Reference Number [#mandate-reference-number] A unique identifier assigned to a mandate by the banking system after creation or authentication. ### Max Amount Cents [#max-amount-cents] The maximum amount that can be collected in a single transaction against a DebiCheck mandate. ### Merchant Profile [#merchant-profile] A configuration record containing the merchant's banking details, approved instruments, and processing settings. ## N [#n] ### Non-Processing Day [#non-processing-day] A day when banks do not process transactions, such as public holidays and bank holidays. ## O [#o] ### OAuth 2.0 [#oauth-20] An authorisation framework used by the Precium API for secure API access. ### Output File [#output-file] A file returned by the bank containing the status of each transaction from an input file. ## P [#p] ### PASA (Payments Association of South Africa) [#pasa-payments-association-of-south-africa] The regulatory body that manages payment systems in South Africa and sets rules for debit order processing. ### Paying Bank [#paying-bank] The bank that holds the customer's (debtor's) account and processes the debit instruction. Also called issuing bank. ### Payment [#payment] An outbound EFT credit transfer from the merchant's account to a recipient's account. ### Profile Code [#profile-code] A code assigned by the bank that links credit limits, account details, and pricing to a merchant. ### Profile Limit [#profile-limit] The maximum total value of transactions a merchant can process per instrument per month. ## R [#r] ### Real-Time Authentication [#real-time-authentication] DebiCheck authentication (TT1) requiring customer response within 120 seconds via USSD or push notification. ### Reconciliation [#reconciliation] The process of matching collection results to bank statement entries to confirm successful settlement. ### Release Date [#release-date] The date from which a mandate becomes active and collections can be processed. ### Reply File [#reply-file] A file from the bank acknowledging receipt and acceptance of an input file for processing. ### RMS (Registered Mandate Service) [#rms-registered-mandate-service] A service that stores authenticated DebiCheck mandates and enables tracking of customer accounts. ## S [#s] ### Service Type [#service-type] The processing speed for EFT transactions: - `SAMEDAY` - Same-day settlement - `ONEDAY` - Next business day settlement - `TWODAY` - Two business day settlement ### Settlement [#settlement] The process of transferring collected or paid funds to the appropriate bank accounts. ### SO (System Operator) [#so-system-operator] A non-bank entity that provides technology services for payment instructions. Funds settle directly to merchant's bank account. ### Sponsoring Bank [#sponsoring-bank] The bank that enables a merchant to collect funds via debit orders. Also called acquiring bank. ### Statement ID [#statement-id] An identifier linking a reconciled collection to a specific bank statement entry. ## T [#t] ### Tracking Period [#tracking-period] For DebiCheck, the number of days (0-10) the bank monitors a customer's account after a failed collection to retry when funds become available. ### TPPP (Third-Party Payment Provider) [#tppp-third-party-payment-provider] An entity that accepts funds from payers for on-payment to third parties. Must be registered with PASA. ### TPPP-Hybrid [#tppp-hybrid] A TPPP model in which collected funds are deposited into the merchant's bank account rather than the TPPP's account. ### TT1 (Transaction Type 1) [#tt1-transaction-type-1] Real-time DebiCheck authentication with a 120-second response window. ### TT1b [#tt1b] Delayed real-time DebiCheck authentication with a same-day response window until 20:00. ### TT2 (Transaction Type 2) [#tt2-transaction-type-2] Batch DebiCheck authentication with a 48-hour response window. ### TT3 (Transaction Type 3) [#tt3-transaction-type-3] DebiCheck authentication using a cheque card and PIN (not currently supported by Precium). ## U [#u] ### Ultimate Creditor [#ultimate-creditor] A secondary recipient in split payment scenarios who receives a portion of the collected funds. ### Unpaid [#unpaid] A collection status indicating the transaction failed, typically due to insufficient funds or account issues. ### USSD (Unstructured Supplementary Service Data) [#ussd-unstructured-supplementary-service-data] A mobile communication protocol used for DebiCheck real-time authentication on feature phones. ## V [#v] ### Value Type [#value-type] For DebiCheck mandates, determines collection amount flexibility: - `FIXED` - No amount variation allowed - `VARIABLE` - Amount can vary up to 1.5x the authenticated amount - `USAGEBASED` - Full flexibility within max amount ### Verify Account [#verify-account] A boolean flag to trigger bank account verification (AVS-R) during mandate creation. ## W [#w] ### Webhook [#webhook] See Callback. ## Z [#z] ### ZAR [#zar] South African Rand - the only currency supported by the Precium Debit Order API. # Going live (/docs/direct-debit/going-live) Requirements and timeline for launching debit order collections in production. ## Prerequisites [#prerequisites] Before going live, ensure you have: - Confirmed processing model (SO, TPPP, or TPPP-Hybrid) - Completed KYB (Know Your Business) and KYC (Know Your Customer) processes - Authorised PASA-approved abbreviated name and/or profile code - Signed MSA (Master Service Agreement) including: - Transactional and service pricing - Approved processing limits ## Processing Models [#processing-models] ### System Operator (SO) [#system-operator-so] Best for: **Large corporations** A non-bank entity providing services for payment instructions. Funds settle directly to your nominated bank account. **Requirements:** - Standard Bank account - Standard Bank profile code and credit limits - PASA vetting and abbreviated name approval - End-to-end testing **Timeline:** ~8 weeks ### Third-Party Payment Provider (TPPP) [#third-party-payment-provider-tppp] Best for: **Small to mid-size businesses** Accept and hold funds for on-payment to third parties. Faster time to market. **Requirements:** - Any bank account - Precium-approved credit limits - PASA vetting and abbreviated name approval - End-to-end testing **Timeline:** ~3 weeks ### TPPP-Hybrid [#tppp-hybrid] Best for: **Mid to large corporates seeking fast market entry** Same as TPPP but collected funds settle into your own bank account. **Requirements:** - Standard Bank account - Precium-approved credit limits - PASA vetting and abbreviated name approval - End-to-end testing **Timeline:** ~3 weeks ## Time to Market [#time-to-market] ### SO Model [#so-model] | Requirement | Timeline | |---|---| | Standard Bank account | 4-6 weeks | | Approved credit limits (Standard Bank) | 2-4 weeks | | Profile code creation and testing | 4-6 weeks | | PASA vetting and abbreviated name | 2-3 weeks | | End-to-end testing | 8 weeks | | **Total** | **~8 weeks** | ### TPPP Model [#tppp-model] | Requirement | Timeline | |---|---| | Any bank account | 0 weeks | | Approved credit limits (Precium) | 1 week | | PASA vetting and abbreviated name | 2-3 weeks | | End-to-end testing | 2 weeks | | **Total** | **~3 weeks** | ### TPPP-Hybrid Model [#tppp-hybrid-model] | Requirement | Timeline | |---|---| | Standard Bank account | 2-3 weeks | | Approved credit limits (Precium) | 1 week | | PASA vetting and abbreviated name | 2-3 weeks | | End-to-end testing | 2 weeks | | **Total** | **~3 weeks** | Note: Platform providers can only use the SO model. ## Documentation Requirements [#documentation-requirements] ### Business Structure [#business-structure] | Document | Notes | |---|---| | Nature of business | e.g., Insurer, Media, Financial Institution | | Company CIPC registration | --- | | Proof of business address | Not older than 3 months (lease, municipal statement) | | SARS Income Tax / VAT registration | --- | | Director resolution | On letterhead, signed by all directors, nominating authorised signatory | | Company shareholding structure | --- | | Copy of ID (signatory/directors/CEO) | --- | | Signatory proof of address | Not older than 3 months | | Key contact phone number | --- | | Key contact email address | --- | ### Banking Information [#banking-information] | Document | Notes | |---|---| | Current bank | --- | | Proof of bank account | For settlement, not older than 3 months | ### Collection Processing History [#collection-processing-history] *If currently processing debit orders:* | Information | Notes | |---|---| | Current client/subscriber base count | e.g., 140,000 | | Current processing bank | --- | | Current processing model | SO/TPPP | | Payment methods required | EFT debit order, DebiCheck, EFT payments | | Statement from current processor | --- | | Unique descriptor/short code | Displayed on customer statements | | Current/expected payment volumes | Monthly count | | Total payment limit required | R'000 | | Average monthly payment value | R'000 | | Batch limit request | R'000 | | Minimum payment value | R'000 | | Maximum payment value | R'000 | ### Settlement Options [#settlement-options] | Information | Notes | |---|---| | Settlement frequency | Daily/Monthly | | Split settlements required | Multiple bank accounts | ## PASA Requirements [#pasa-requirements] ### Abbreviated Name [#abbreviated-name] Your abbreviated name: - Appears on customer bank statements - Must be unique and PASA-approved - Maximum 10 characters - Should reflect your trading name ### Registration [#registration] All TPPPs must be registered with a clearing participant. Verify your registration status at [PASA Authorisations](https://www.pasa.org.za/authorisations). ## Acquiring Bank [#acquiring-bank] Currently, Precium supports debit order collections through **Standard Bank** as the acquiring bank. SO and TPPP-Hybrid processing models require merchants to open a **Standard Bank account** for settlement purposes. ## Go-Live Checklist [#go-live-checklist] ### Technical Readiness [#technical-readiness] - Completed all sandbox testing scenarios - Tested webhook integrations - Implemented error handling - Set up monitoring and alerting - Documented integration in your systems ### Business Readiness [#business-readiness] - Staff trained on debit order processes - Customer support prepared for mandate queries - Mandate templates meet PASA requirements - Dispute handling process documented - Reconciliation process established ### Compliance [#compliance] - Mandate storage solution in place (7-year retention) - PASA rules understood and documented - Privacy policy updated for debit order processing ## Production Credentials [#production-credentials] When ready to launch: 1. Contact your Precium account manager 2. Complete final compliance review 3. Receive production API credentials 4. Update application configuration: ```bash # Production authentication endpoint https://prod-payce.auth.eu-west-1.amazoncognito.com/oauth2/token # Production API endpoint # Provided by Precium upon approval ``` ## Post Go-Live Support [#post-go-live-support] ### Monitoring [#monitoring] - Monitor collection success rates - Track unpaid and dispute rates - Review reconciliation status daily ### Support Channels [#support-channels] - Technical issues: Contact Precium support - Business queries: Contact your account manager - Emergency: Escalation contacts provided at go-live ## Contact [#contact] To initiate the go-live process: - **Email**: support@precium.com - **Sales**: Contact your Precium account manager Tip: Reach out early to avoid delays. The sooner you start the onboarding process, the faster you can go live. # Integration quick start (/docs/direct-debit/integration-quick-start) ## Overview [#overview] The Precium Debit Order API enables you to generate ad-hoc or recurring debit order payments and collections. This guide will help you make your first API call. ## Prerequisites [#prerequisites] Before you begin, you will need: 1. **API Credentials**: Contact our sales team at support@precium.com to request your sandbox username and password 2. **Postman or HTTP Client**: For testing API requests 3. **Understanding of South African debit orders**: Familiarity with EFT and DebiCheck payment instruments ## Step 1: Obtain Access Token [step] [#step-1-obtain-access-token-step] All API requests require a Bearer token. Generate one using OAuth 2.0 Client Credentials flow: ```bash curl -X POST https://dev-payce.auth.eu-west-1.amazoncognito.com/oauth2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -u "YOUR_USERNAME:YOUR_PASSWORD" \ -d "grant_type=client_credentials" ``` **Response:** ```json { "access_token": "eyJraWQiOiJ...", "expires_in": 86400, "token_type": "Bearer" } ``` Note: Access tokens expire every 24 hours. ## Step 2: Get Your Merchant Profile [step] [#step-2-get-your-merchant-profile-step] Retrieve your merchant profile to obtain the required configuration values: ```bash curl -X GET https://dev.api.revio.co.za/v2/merchantprofile/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json [ { "id": "24", "profileCode": "TEST1", "abbreviatedName": "TESTMERCH1", "accountNumber": "12345678", "branchCode": "051001", "name": "TEST MERCHANT 1", "instruments": ["EFTONEDAY", "EFTTWODAY", "EFTSAMEDAY", "DEBICHECK"], "currency": "ZAR", "entryClass": "21" } ] ``` Store these values—you'll need them for subsequent API calls: - `profileCode` - `abbreviatedName` - `accountNumber` (creditor account) - `branchCode` (creditor branch) - `name` (creditor name) ## Step 3: Create Your First EFT Mandate [step] [#step-3-create-your-first-eft-mandate-step] Create a mandate to authorise future collections from a customer: ```bash curl -X POST https://dev.api.revio.co.za/v2/mandate/eft/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "defaultServiceType": "ONEDAY", "profileCode": "TEST1", "abbreviatedName": "TESTMERCH1", "contractReference": "CONTRACT001", "debtor": { "accountNumber": "010553922", "accountType": "CURRENT", "bank": "ABSA", "branchCode": "632005", "firstName": "John", "lastName": "Smith", "identification": { "emailAddress": "john@example.com", "idNumber": "0001010000006", "phoneNumber": "+27-123456789" } }, "creditor": { "accountNumber": "12345678", "accountType": "CURRENT", "branchCode": "051001", "name": "TEST MERCHANT 1", "bank": "STANDARDBANK", "phoneNumber": "+27-615333440", "emailAddress": "merchant@test.com" }, "frequency": "MONTHLY", "collectionDay": 15, "amountCents": 10000, "currency": "ZAR", "entryClass": "21" }' ``` **Response:** ```json { "id": "8f4a6b0f-fa9b-4f45-8e19-b75c1a70432a", "status": "ACTIVE", "contractReference": "CONTRACT001", ... } ``` ## Step 4: Create a Collection [step] [#step-4-create-a-collection-step] Use the mandate ID to create a collection: ```bash curl -X POST https://dev.api.revio.co.za/v2/collection/eft/from_mandate/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "mandateId": "8f4a6b0f-fa9b-4f45-8e19-b75c1a70432a", "collectionDate": "2024-02-15T00:00:00.000Z", "amountCents": 10000, "serviceType": "ONEDAY" }' ``` **Response:** ```json { "id": "63d94be3-1faf-428b-89f3-bc5271be8015", "status": "PENDING", ... } ``` ## Step 5: Check Collection Status [step] [#step-5-check-collection-status-step] Monitor the collection status: ```bash curl -X GET https://dev.api.revio.co.za/v2/collection/eft/63d94be3-1faf-428b-89f3-bc5271be8015/status/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ## Quick Reference [#quick-reference] | Action | Endpoint | Method | |---|---|---| | Get Token | `/oauth2/token` | POST | | Get Merchant Profile | `/v2/merchantprofile/` | GET | | Create EFT Mandate | `/v2/mandate/eft/` | POST | | Create DebiCheck Mandate | `/v2/mandate/debicheck/` | POST | | Create EFT Collection | `/v2/collection/eft/from_mandate/` | POST | | Create DebiCheck Collection | `/v2/collection/debicheck/from_mandate` | POST | | Create EFT Payment | `/v2/payment/eft` | POST | ## Next Steps [#next-steps] - Authentication - Learn about token management - EFT Debit Orders - Detailed EFT mandate and collection guide - DebiCheck - Authenticated debit order collections - Testing - Test scenarios and mock bank simulation ## Resources [#resources] - **Postman Collection**: Available for download from the Precium technical reference documentation # Introduction (/docs/direct-debit/introduction) ## Debit Order API Documentation [#debit-order-api-documentation] Welcome to the Precium Debit Order API documentation. This API enables you to process debit order collections and EFT payments in South Africa. Our Debit Order API is built on a comprehensive platform that handles the full complexity of South African payment processing. While you interact with our REST API endpoints, these backend services work together to ensure reliable, compliant, and automated debit order processing at scale. ## Platform Services [#platform-services] | Service | Features & Capabilities | |---|---| | **Authentication service** | Secure OAuth2 implementation using bearer tokens for API authentication accross merchant profiles. | | **Scheduling service** | Provides automated cut-off management for both internal and external timelines, with intelligent acquirer submission window management to ensure timely processing. | | **Mandate management service** | Handles complete mandate lifecycle automation including mandate amendment processing and re-authentication workflows to maintain compliance and accuracy. | | **Batching service** | Enables flexible user set management with support for multi-profile batch submission, allowing batching by account, userset, and abbreviated name. | | **Cut-off management service** | Manages acquirer submission windows automatically, handles service type cutoff scheduling, and accounts for non-processing days to prevent failed submissions. | | **Submission service** | Executes automated schedule processing with built-in recovery on processing failures. Handles bank response irregularities and ensures robustness on submissions and resubmissions within cutoff windows, with submission idempotency guarantees. | | **Results mapping service** | Performs timeous mapping of result sets and manages result and dispute files. Tracks authentication results for both realtime and batch transactions with optimized large dataset management. | | **Reconciliation service** | Provides automated mapping and reconciliation between reported results and bank statement results, with discrepancy detection and resolution capabilities. | | **Monitoring service** | Offers real-time uptime monitoring with proactive fault detection and exception reporting and alerting across all system services. | | **Webhook service** | Delivers real-time callback alerting for transaction status updates and mandate lifecycle notifications through configurable webhook endpoints per client system. | | **Multi-merchant capability** | Supports management of multiple merchant profiles with abbreviated name separation, account-level separation of concerns, and independent configuration per merchant. | | **Multi-acquirer integration layer** | Provides seamless integration with all South African acquirers, supporting different SFTP protocols and authentication standards. Handles file pattern normalisation across acquirers and standardises transaction and authentication results. | ## What This Means for You [#what-this-means-for-you] ✓ **Simplified Integration** - You only need to integrate with our single, unified API ✓ **Automatic Compliance** - Cut-offs, windows, and banking rules are handled automatically ✓ **Reliability** - Built-in retry logic, error handling, and reconciliation ✓ **Scale** - Designed to handle high volumes with batch processing and large dataset support ✓ **Multi-Bank** - One API works with all major South African banks ✓ **Real-Time Updates** - Webhook integration keeps your systems synchronised ✓ **White-Label Ready** - Platforms can easily build their own branded debit order acquiring services on top of our infrastructure, with multi-merchant and multi-profile support enabling complete service separation ## Documentation Structure [#documentation-structure] | Document | Description | |---|---| | Quickstart | Get up and running quickly | | Authentication | API access and token management | | Merchant Profiles | Managing merchant configurations | | EFT Debit Orders | EFT mandate and collection management | | DebiCheck | DebiCheck mandate and collection management | | EFT Payments | Outbound EFT credit payments | | Webhooks | Event notifications and callbacks | | Testing | Sandbox environment and test scenarios | | Going Live | Production readiness checklist | | Response Codes | API status codes and error handling | | Risk management | PASA rules, glossary, and additional information | | Troubleshooting | Common issues and solutions | | Glossary | Key terms | ## API Base URLs [#api-base-urls] | Environment | URL | |---|---| | Sandbox | `https://dev.api.revio.co.za/` | | Production | `https://api.revio.co.za/` | ## API version [#api-version] Two versions are served in production. **Build against v2.** It is a superset — 88 operations against v1's 50 — and carries account verification, bulk CSV upload and download, DebiCheck mandate amend, poll and reset, settlement and bank-account statements, mandate audit and reporting. v1 remains available for existing integrations and is not being extended. The version sits in the path, so the two are parallel trees rather than successive revisions: `/v1/…` and `/v2/…` share no endpoints. Every example in this documentation uses v2. | Environment | Base URL | Token endpoint | |---|---|---| | Sandbox | `https://dev.api.revio.co.za/` | `https://dev-payce.auth.eu-west-1.amazoncognito.com/oauth2/token` | | Production | `https://api.revio.co.za/` | `https://prod-payce.auth.eu-west-1.amazoncognito.com/oauth2/token` | Interactive reference: [api.revio.co.za/swagger](https://www.api.revio.co.za/swagger/index.html) (production) and [dev.api.revio.co.za/swagger](https://dev.api.revio.co.za/swagger/index.html) (sandbox). ## Key Features [#key-features] - **EFT Debit Orders**: Collect recurring payments via standard EFT debit orders - **DebiCheck**: Authenticated debit orders with reduced disputability - **EFT Payments**: Same-day, one-day, and two-day outbound payments - **Bank Account Verification**: Validate customer bank details before collection - **Webhooks**: Real-time notifications for transaction status changes - **Automated Reconciliation**: Match collections to bank statements automatically ## Support [#support] For technical support or to request API credentials, contact: - Email: [support@precium.com](mailto:support@precium.com) ## Version [#version] Current API Version: **v2** # Merchant profiles (/docs/direct-debit/merchant-profiles) Merchant profiles store your banking configuration and are required for processing collections and payments. ## Overview [#overview] A merchant profile contains: - Your bank account details for settlement - Approved collection instruments (EFT, DebiCheck) - Your PASA-registered abbreviated name - Processing limits and configuration ## Get Merchant Profile [#get-merchant-profile] Retrieve your merchant profile(s) to obtain the configuration values needed for API calls. ### Endpoint [#endpoint] ```json GET https://api.revio.co.za/v2/merchantprofile/ ``` ### Request [#request] ```bash curl -X GET https://dev.api.revio.co.za/v2/merchantprofile/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" ``` ### Response [#response] ```json [ { "id": "24", "profileCode": "TEST1", "bank": "STANDARDBANK", "instruments": ["EFTONEDAY", "EFTTWODAY", "EFTSAMEDAY", "DEBICHECK"], "name": "TEST MERCHANT 1", "accountNumber": "12345678", "branchCode": "051001", "abbreviatedName": "TESTMERCH1", "contactNumber": "+27-615333440", "emailAddress": "test@test.com", "currency": "ZAR", "entryClass": "21", "createdByClient": "109" } ] ``` ### Response Fields [#response-fields] | Field | Type | Description | |---|---|---| | `id` | string | Unique merchant profile identifier | | `profileCode` | string | Bank-provided profile code linking credit limits and pricing | | `bank` | string | Bank name (e.g., `STANDARDBANK`) | | `instruments` | array | Enabled payment instruments | | `name` | string | Merchant trading name registered with bank | | `accountNumber` | string | Settlement account number | | `branchCode` | string | Bank branch code | | `abbreviatedName` | string | PASA-approved name shown on customer bank statements | | `contactNumber` | string | Merchant contact phone number | | `emailAddress` | string | Merchant email address | | `currency` | string | Currency code (always `ZAR`) | | `entryClass` | string | Entry class code (always `21`) | | `createdByClient` | string | Parent client ID | ### Available Instruments [#available-instruments] | Instrument | Description | |---|---| | `EFTSAMEDAY` | Same-day EFT collection/payment | | `EFTONEDAY` | One business day EFT collection/payment | | `EFTTWODAY` | Two business day EFT collection/payment | | `DEBICHECK` | DebiCheck authenticated debit orders | ## Get Merchant Profile by ID [#get-merchant-profile-by-id] Retrieve a specific merchant profile. ### Endpoint [#endpoint-1] ```text GET https://api.revio.co.za/v2/merchantprofile/{id}/ ``` ### Request [#request-1] ```bash curl -X GET https://dev.api.revio.co.za/v2/merchantprofile/24/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` ## Sub-merchant profiles (platform providers) [#sub-merchant-profiles-platform-providers] Platform providers operating under the System Operator model can have merchant profiles created for their sub-merchants. **There is no API operation to create a merchant profile.** The Mandates API exposes only `GET /v2/merchantprofile` and `GET /v2/merchantprofile/{id}`. Profiles are created by Precium as part of onboarding; contact support to have the capability enabled and the profiles provisioned. Once they exist, read them with the two endpoints above. ## Using Profile Values [#using-profile-values] The merchant profile values are required when creating mandates and collections: ```json { "profileCode": "TEST1", // From merchant profile "abbreviatedName": "TESTMERCH1", // From merchant profile "creditor": { "accountNumber": "12345678", // From merchant profile "branchCode": "051001", // From merchant profile "name": "TEST MERCHANT 1", // From merchant profile "bank": "STANDARDBANK" } } ``` ## Abbreviated Name [#abbreviated-name] The abbreviated name is a critical identifier that: - Appears on customer bank statements - Must be registered and approved by PASA - Uniquely identifies your business to customers - Is limited to 10 characters Example bank statement display: ```text TESTMERCH1 CONTRACT001 -R100.00 ``` ## Test Merchant Profile [#test-merchant-profile] For sandbox testing, use the following pre-configured profile: | Field | Value | |---|---| | `id` | `24` | | `profileCode` | `TEST1` | | `abbreviatedName` | `TESTMERCH1` | | `accountNumber` | `12345678` | | `branchCode` | `051001` | | `name` | `TEST MERCHANT 1` | ## Platform Provider Requirements [#platform-provider-requirements] To create and manage merchant profiles as a platform provider, you need: 1. **Standard Bank Profile Code** and credit limits 2. **Standard Bank processing account** for real-time settlement 3. **PASA Registration** as a Third-Party Payment Provider (TPPP) 4. **Precium verification** and approval Contact Precium support to enable platform provider capabilities. # Response codes (/docs/direct-debit/response-codes) HTTP status codes and error handling for the Precium API. ## HTTP Status Codes [#http-status-codes] | Code | Type | Description | |---|---|---| | `200` | Successful | Request processed successfully | | `201` | Created | Resource created successfully | | `204` | No Content | Request processed, resource removed | | `400` | Bad Request | Invalid request structure or data | | `401` | Unauthorized | Invalid or expired access token | | `403` | Forbidden | Insufficient permissions | | `404` | Not Found | Requested resource not found | | `409` | Conflict | Resource conflict (e.g., duplicate reference) | | `500` | Internal Server Error | Server error - contact Precium support | ## Success Responses [#success-responses] ### 200 OK [#200-ok] Returned for successful GET requests and updates. ```json { "id": "8f4a6b0f-fa9b-4f45-8e19-b75c1a70432a", "status": "COMPLETED", ... } ``` ### 201 Created [#201-created] Returned when a resource is successfully created. ```json { "id": "new-resource-id", "status": "PENDING", ... } ``` ### 204 No Content [#204-no-content] Returned when a resource is successfully deleted. No response body. ## Error Responses [#error-responses] ### 400 Bad Request [#400-bad-request] Invalid request data. Check required fields and formats. ```json { "value": { "Message": "Validation error: field 'accountNumber' is required" }, "statusCode": 400 } ``` **Common causes:** - Missing required fields - Invalid field formats - Invalid enum values - Business rule violations ### 401 Unauthorized [#401-unauthorized] Invalid or expired access token. ```json { "message": "Unauthorized" } ``` **Resolution:** Request a new access token. ### 403 Forbidden [#403-forbidden] Insufficient permissions for the requested operation. ```json { "message": "Forbidden" } ``` **Resolution:** Contact Precium support to verify account permissions. ### 404 Not Found [#404-not-found] Requested resource does not exist. ```json { "message": "Resource not found" } ``` **Common causes:** - Invalid resource ID - Resource deleted or expired ### 409 Conflict [#409-conflict] Resource conflict, typically duplicate data. ```json { "message": "Duplicate external reference" } ``` **Common causes:** - Duplicate `externalReference` - Duplicate `contractReference` ### 500 Internal Server Error [#500-internal-server-error] Server-side error. ```json { "message": "Internal server error" } ``` **Resolution:** Contact Precium support with request details. ## Mandate Status Codes [#mandate-status-codes] ### EFT Mandate Statuses [#eft-mandate-statuses] | Status | Description | |---|---| | `ACTIVE` | Mandate is active and can be used for collections | | `SUSPENDED` | Mandate temporarily suspended | | `CANCELLED` | Mandate cancelled, no further collections | | `EXPIRED` | Mandate has expired | ### DebiCheck Mandate Statuses [#debicheck-mandate-statuses] | Status | Description | |---|---| | `NEW` | Mandate created, not yet submitted | | `PENDING_AUTHENTICATION` | Awaiting customer authentication | | `COMPLETED` | Successfully authenticated | | `AUTH_FAILURE` | Authentication failed or rejected | | `CANCELLED` | Mandate cancelled | | `SUSPENDED` | Mandate temporarily suspended | ## Collection Status Codes [#collection-status-codes] | Status | Description | |---|---| | `PENDING` | Collection submitted, awaiting processing | | `PROCESSING` | Being processed by bank | | `COMPLETED` | Collection successful | | `UNPAID` | Collection failed | | `DISPUTED` | Customer disputed the collection | | `CANCELLED` | Collection cancelled before processing | ### Unpaid Reason Codes [#unpaid-reason-codes] When a collection returns `UNPAID`, additional information may include: | Code | Description | |---|---| | `INSUFFICIENT_FUNDS` | Not enough funds in account | | `ACCOUNT_CLOSED` | Account has been closed | | `ACCOUNT_NOT_FOUND` | Account does not exist | | `PAYMENT_STOPPED` | Customer stopped the payment | | `NOT_PROVIDED_FOR` | Account holder rejected debit | ## Payment Status Codes [#payment-status-codes] | Status | Description | |---|---| | `PENDING` | Payment submitted, awaiting processing | | `PROCESSING` | Being processed by bank | | `COMPLETED` | Payment successful | | `FAILED` | Payment failed | | `CANCELLED` | Payment cancelled | ## Bank Account Verification Codes [#bank-account-verification-codes] When `verifyAccount: true` fails: ```json { "value": { "Message": "Debtor account verification failure: details={\"AccountExists\":true,\"AccountIsOpen\":false,\"AccountTypeMatches\":null,\"IdNumberMatches\":null,\"AccountAcceptsDebits\":null}" }, "statusCode": 400 } ``` | Field | Description | |---|---| | `AccountExists` | Account number exists at bank | | `AccountIsOpen` | Account is active/open | | `AccountTypeMatches` | Provided type matches actual | | `AccountAcceptsDebits` | Account can receive debits | | `IdNumberMatches` | ID matches account holder | ## Error Handling Best Practices [#error-handling-best-practices] ### Retry Logic [#retry-logic] ```jsx async function apiRequest(options, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch(options.url, options); if (response.status === 401) { // Refresh token and retry await refreshToken(); continue; } if (response.status >= 500) { // Server error - wait and retry await sleep(attempt * 1000); continue; } return response; } catch (error) { if (attempt === maxRetries) throw error; await sleep(attempt * 1000); } } } ``` ### Logging [#logging] Log all API responses for debugging: ```jsx console.log({ timestamp: new Date().toISOString(), endpoint: response.url, status: response.status, requestId: response.headers.get('x-request-id'), body: await response.json() }); ``` ### Error Mapping [#error-mapping] Map API errors to user-friendly messages: ```jsx const errorMessages = { 400: 'Invalid request data. Please check your input.', 401: 'Session expired. Please log in again.', 403: 'You do not have permission for this action.', 404: 'The requested resource was not found.', 409: 'This reference already exists.', 500: 'An unexpected error occurred. Please try again later.' }; ``` ## Contact Support [#contact-support] If you encounter a `500` status code or persistent errors: 1. Note the request details (endpoint, payload, timestamp) 2. Check for any `x-request-id` in response headers 3. Contact Precium support with this information **Email:** support@precium.com # Risk management (/docs/direct-debit/risk-management) ## PASA Rules [#pasa-rules] The Payments Association of South Africa (PASA) enforces strict rules for debit order processing. Compliance is mandatory to avoid penalties and processing disruptions. ### Valid Mandates [#valid-mandates] Every debit order requires a valid mandate that complies with PASA and bank minimum requirements. **Penalties:** R1,000 per non-compliant mandate during PASA spot audits. ### Mandate Retention [#mandate-retention] Retain all mandates for **7 years** after the last transaction. PASA can audit any merchant account at random. ### Abbreviated Name [#abbreviated-name] Your abbreviated name: - Must be PASA-registered - Appears on customer bank statements - Should accurately reflect your trading name - Maximum 10 characters ## Minimum Mandate Requirements [#minimum-mandate-requirements] ### Paper Mandates [#paper-mandates] Requirement Account number Type of account Branch number Frequency of debit Abbreviated name on statement Contract reference Commencement/action date Start and termination date Beneficiary name Beneficiary contact details Amount (escalation, variable, max limits) Signature of account holder ### Voice Mandates [#voice-mandates] Requirement Abbreviated name on statement Beneficiary name Call centre name Contact telephone number Contract reference number Current date Commencement/action date Payer's bank details Surname and initials of account holder Value of debit amount **Additional voice mandate requirements:** - Written confirmation within 21 days before first debit - Inform customer of recording - Inform customer of contract reference and abbreviated name - Use reliable, preferably SABS-approved recording devices - Index recordings for easy retrieval ### Electronic Mandates [#electronic-mandates] Requirement Account number Type of account Branch number Frequency of debit Abbreviated name on statement Contract reference Commencement/action date Start and termination date Beneficiary name Beneficiary contact details Amount (escalation, variable, max limits) Electronic signature of account holder IP address and location coordinates ## Managing Unpaid Collections [#managing-unpaid-collections] PASA rules for handling failed collections: | Scenario | Rule | |---|---| | Returned "Unpaid" | Cannot resubmit until next mandated date | | 2 consecutive "Not provided for" | Cannot resubmit without new mandate | | "Account Closed" or "Payment Stopped" | Cannot resubmit at all | | System/technical error | May resubmit after corrections | **Unpaid timeframe:** Other banks return unpaid instructions within **4 business days** of action date (EFT only). ## Managing Disputes [#managing-disputes] Customers can dispute both EFT and DebiCheck collections: | Timeframe | Process | |---|---| | Within 40 days | Bank returns transaction immediately | | After 40 days | Bank notifies merchant, 30 days to provide mandate | | No mandate provided | Bank debits merchant, refunds customer | ### Dispute Reasons [#dispute-reasons] - The transaction lacks proper mandate authorisation - Transaction violates mandate terms - The mandate was cancelled Note: For voice-recorded mandates, bank immediately debits merchant upon dispute notification. ## Cut-off Times Summary [#cut-off-times-summary] | Service | Weekday | Weekend | |---|---|---| | EFT Same-day | 08:00-16:20 | 08:00-09:45 | | EFT One-day | 08:00-16:00 | 08:00-10:15 | | EFT Two-day | 08:00-14:30 (2 days prior) | 08:00-09:00 | | DebiCheck/RMS | 08:00-16:00 (2 days prior) | 08:00-11:00 (2 days prior) | | DebiCheck TT1/TT2 | 03:00-18:30 | 03:00-18:30 | | AVS-R | 24 hours | 24 hours | ## Bank Codes [#bank-codes] ### Bank Names (API Format) [#bank-names-api-format] | Bank | API Value | |---|---| | ABSA Bank | `ABSA` | | Standard Bank | `STANDARDBANK` | | First National Bank | `FNB` | | Nedbank | `NEDBANK` | | Capitec Bank | `CAPITEC` | | Investec Bank | `INVESTEC` | | Bidvest Bank | `BIDVEST` | ### Common Branch Codes [#common-branch-codes] | Bank | Branch Code | |---|---| | ABSA | `632005`, `632500` | | Standard Bank | `051001` | | FNB | `250655` | | Nedbank | `198765` | | Capitec | `470010` | | Investec | `580105` | # Technical reference documentation (/docs/direct-debit/technical-reference-documentation) ## About this reference [#about-this-reference] The endpoint reference for the Precium Direct Debit (Mandates) API. Interactive documentation and a Postman collection are linked below; the tables on this page are the quick reference. | Resource | Link | |---|---| | **Interactive reference — production** | [api.revio.co.za/swagger](https://www.api.revio.co.za/swagger/index.html) | | **Interactive reference — sandbox** | [dev.api.revio.co.za/swagger](https://dev.api.revio.co.za/swagger/index.html) | | **Postman collection** | [Debit Order API](https://www.postman.com/reviopay/revio-debit-order-api/collection/uipx6bl/debit-order-api) | ## Environments [#environments] | Environment | Base URL | Token endpoint | |---|---|---| | Sandbox | `https://dev.api.revio.co.za/` | `https://dev-payce.auth.eu-west-1.amazoncognito.com/oauth2/token` | | Production | `https://api.revio.co.za/` | `https://prod-payce.auth.eu-west-1.amazoncognito.com/oauth2/token` | Unlike the card APIs, sandbox and production are **separate hosts**. Credentials are issued per environment and are not interchangeable. ## API version [#api-version] Two versions are served. **Build against v2** — 88 operations against v1's 50, and the only version carrying account verification, bulk CSV, DebiCheck amend, poll and reset, settlement and statement data, mandate audit and reporting. v1 remains available for existing integrations and is not being extended. The version is part of the path, so the two are parallel trees that share no endpoints. Every path below is v2 and is relative to the base URL. ## Authentication [#authentication] OAuth 2.0 client credentials. POST to the token endpoint with HTTP Basic authentication, then send the returned JWT as a bearer token. ```bash curl -X POST https://prod-payce.auth.eu-west-1.amazoncognito.com/oauth2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d "grant_type=client_credentials" ``` ```text Authorization: Bearer <access_token> ``` Tokens are valid for 24 hours (`expires_in: 86400`). Cache and reuse one rather than requesting per call, and refresh on `401`. ## Mandates [#mandates] A mandate is the payer's authority to collect. Create one before collecting. | Method | Path | Purpose | |---|---|---| | GET, POST | `/v2/mandate/debicheck` | List or create DebiCheck mandates | | GET, PUT, PATCH | `/v2/mandate/debicheck/{mandateId}` | Retrieve or update a mandate | | POST | `/v2/mandate/debicheck/{mandateId}/auth` | Authenticate the mandate with the payer | | POST | `/v2/mandate/debicheck/{mandateId}/amend` | Amend an authenticated mandate | | POST | `/v2/mandate/debicheck/{mandateId}/poll` | Poll the bank for an authentication result | | POST | `/v2/mandate/debicheck/{mandateId}/reset` | Reset to the previous valid state | | GET | `/v2/mandate/debicheck/{mandateId}/status` | Latest status | | GET, POST | `/v2/mandate/eft` | List or create EFT mandates | | GET, PUT, PATCH, DELETE | `/v2/mandate/eft/{mandateId}` | Retrieve, update or delete | | GET | `/v2/mandate/eft/{mandateId}/status` | Latest status | | GET, POST | `/v2/mandateresult` | Mandate results | | GET, POST | `/v2/mandateaudit` | Mandate audit trail | | GET, DELETE | `/v2/mandateaudit/{id}` | A specific audit record | `amend` sets `requiresAuth` internally — do not set it yourself. ## Collections [#collections] A collection debits a payer under an existing mandate. | Method | Path | Purpose | |---|---|---| | GET, POST | `/v2/collection/debicheck` | List or create DebiCheck collections | | POST | `/v2/collection/debicheck/from_mandate` | Create from an existing mandate | | GET, DELETE | `/v2/collection/debicheck/{collectionId}` | Retrieve or cancel | | GET | `/v2/collection/debicheck/{collectionId}/status` | Latest status | | POST | `/v2/collection/debicheck/{collectionId}/recreate_as_eft` | Recreate a failed DebiCheck collection as EFT | | GET, POST | `/v2/collection/eft` | List or create EFT collections | | POST | `/v2/collection/eft/from_mandate` | Create from an existing mandate | | GET, DELETE | `/v2/collection/eft/{collectionId}` | Retrieve or cancel | | GET | `/v2/collection/eft/{collectionId}/status` | Latest status | | GET, POST | `/v2/collectionresult` | Collection results | ## Payments [#payments] Outbound EFT credits to a bank account. | Method | Path | Purpose | |---|---|---| | GET, POST | `/v2/payment/eft` | List or create EFT payments | | GET, DELETE | `/v2/payment/eft/{paymentId}` | Retrieve or cancel | | GET | `/v2/payment/eft/{paymentId}/status` | Latest status | ## Bulk operations by CSV [#bulk-operations-by-csv] Mandates, collections and payments can all be created in bulk. The pattern is identical for each: generate a template, upload it, then download the processed results. | Method | Path pattern | Purpose | |---|---|---| | GET | `/v2/{mandate\|collection\|payment}/{type}/upload/csv/generate_template` | Download the correct CSV template | | POST | `/v2/{mandate\|collection\|payment}/{type}/upload/csv` | Submit a populated CSV; returns a `bulkUploadId` | | GET | `/v2/{mandate\|collection\|payment}/{type}/download/csv/{bulkUploadId}` | Download the processed results | Always generate the template rather than constructing a CSV by hand — column order and headers are validated on upload. ## Account verification [#account-verification] | Method | Path | Purpose | |---|---|---| | POST | `/v2/accountverification/validate` | Validate a bank account's structure | | POST | `/v2/accountverification/verify` | Verify the account exists and matches the holder | Verify before creating a mandate. An unverifiable account produces a mandate that will fail at collection. ## Profiles [#profiles] | Method | Path | Purpose | |---|---|---| | GET | `/v2/merchantprofile` | List merchant profiles | | GET | `/v2/merchantprofile/{id}` | A specific merchant profile | | GET | `/v2/clientprofile` | List client profiles | | GET | `/v2/clientprofile/{id}` | A specific client profile | `GET /v2/merchantprofile` is the simplest call to confirm a token works. ## Callbacks [#callbacks] Register the URL you want events delivered to, then use the test endpoints to confirm your handler before going live. | Method | Path | Purpose | |---|---|---| | GET, POST | `/v2/clientcallbackurl` | List or register your callback URL | | GET, PUT, DELETE | `/v2/clientcallbackurl/{id}` | Retrieve, update or remove a registration | | POST | `/v2/callback/test/mandate/debicheck/{mandateId}` | Trigger a test mandate callback | | POST | `/v2/callback/test/collection/debicheck/{collectionId}` | Trigger a test DebiCheck collection callback | | POST | `/v2/callback/test/collection/eft/{collectionId}` | Trigger a test EFT collection callback | | POST | `/v2/callback/test/payment/eft/{paymentId}` | Trigger a test EFT payment callback | | POST | `/v2/callback/test/settlement/{settlementId}` | Trigger a test settlement callback | **This API does not sign its callbacks.** There is no signature header and no public key, so a callback cannot be authenticated from its content alone. Treat every callback as an untrusted notification: use it as a prompt to re-read the authoritative state from the relevant `/status` endpoint, and never act on the payload's values directly. This differs from the card APIs, which do sign callbacks — do not carry that assumption across. ## Events [#events] Polling alternatives to callbacks, each ordered by time so you can resume from where you left off. | Method | Path | Purpose | |---|---|---| | GET | `/v2/event/mandate` | Mandate events | | GET | `/v2/event/collection` | Collection events | | GET | `/v2/event/payment` | Payment events | | GET | `/v2/event/profile` | Profile events | Because callbacks are unsigned, the event endpoints are the more trustworthy basis for reconciliation. ## Settlement and reporting [#settlement-and-reporting] | Method | Path | Purpose | |---|---|---| | GET | `/v2/reporting` | Reports per profile | | GET | `/v2/transactionfile` | List transaction files | | GET | `/v2/transactionfile/{id}` | A specific transaction file | | GET | `/v2/bankaccountstatement/{bankAccountNumber}` | Statements for an account | | GET | `/v2/bankaccountstatement/{bankAccountNumber}/{id}` | A specific statement | | GET | `/v2/bankaccountmarkoff` | Mark-off details | | GET | `/v2/bankaccountsettlement/config/{configId}/execute` | Settlement executions for a configuration | ## Conventions [#conventions] | Topic | Behaviour | |---|---| | **Authentication** | Bearer JWT from Cognito; 24-hour validity; refresh on `401` | | **Environments** | Separate hosts and separate credentials for sandbox and production | | **Version** | In the path. Build against `/v2/` | | **Status** | Every object type exposes a `/status` endpoint — the authoritative source | | **Callbacks** | Unsigned. Use as a prompt to re-read state, not as a source of truth | | **Bulk** | Generate template → upload → download results, keyed by `bulkUploadId` | ## Status codes [#status-codes] | Code | Meaning | |---|---| | `200` / `201` | Success / created | | `400` | Validation error — check the response body for the failing field | | `401` | Token missing, malformed or expired. Request a new one | | `403` | Token valid but not permitted for this profile | | `404` | Not found, or the wrong API version in the path | | `500` | Server error. Safe to retry reads | Response codes specific to mandates and collections are in *Response codes*; diagnostics are in *Troubleshooting*. # Testing (/docs/direct-debit/testing) The Revio sandbox environment provides a mock-bank infrastructure for comprehensive testing. ## Overview [#overview] The sandbox allows you to: - Test mandate creation, collections, and payments - Simulate bank processing and status changes - Test error scenarios and edge cases - Verify webhook integrations ## Getting Started [#getting-started] ### Request Test Credentials [#request-test-credentials] Contact Precium at support@precium.com to request sandbox credentials: - `client_id` (username) - `client_secret` (password) ### Test Resources [#test-resources] | Resource | Location | |---|---| | Postman Collection | [Download](https://revio-direct-debit-order-api.readme.io/) | | Swagger Documentation | `https://dev.api.revio.co.za/swagger/index.html` | | Sandbox API | `https://dev.api.revio.co.za/` | | Production API | `https://api.revio.co.za/` — do not test against this | ## Test Merchant Profile [#test-merchant-profile] Use this pre-configured profile for sandbox testing: | Field | Value | |---|---| | `id` | `24` | | `profileCode` | `TEST1` | | `abbreviatedName` | `TESTMERCH1` | | `accountNumber` | `12345678` | | `branchCode` | `051001` | | `name` | `TEST MERCHANT 1` | | `instruments` | `EFTONEDAY`, `EFTTWODAY`, `EFTSAMEDAY`, `DEBICHECK` | | `currency` | `ZAR` | | `entryClass` | `21` | | `createdByClient` | `109` | ## Mock Bank Simulation [#mock-bank-simulation] The mock bank simulates real bank processing, including: - Status changes (PENDING → COMPLETED/UNPAID) - Business rules enforcement - Response file generation ### Simulation Scenarios [#simulation-scenarios] | Instrument | Available Tests | |---|---| | EFT Debit Order | Create mandate, amend mandate, create collection | | DebiCheck | Create mandate (TT1/TT2), amend mandate, cancel/suspend, create collection | | EFT Payments | Create payment, delete payment | ## EFT Collection Test Scenarios [#eft-collection-test-scenarios] Use specific account numbers to simulate different outcomes: ### Account Number Scenarios [#account-number-scenarios] | Account Number | Scenario | Description | |---|---|---| | `910000001` | Full reject | Receive full rejection response | | `910000002` | Batch rejection | Receive batch rejection response | | `210000001` | Dispute | Dispute file 40 seconds after response | | `110000001` | Accepted | Accepted file 40 seconds after pending | | `110000002` | Unpaid | Unpaid file 40 seconds after pending | | `110000003` | Accepted (3 days) | Accepted with settlement 3 days out | | `110000004` | Unpaid (3 days) | Unpaid with settlement 3 days out | | `110000005` | Accepted (5 days) | Accepted with settlement 5 days out | | `110000006` | Unpaid (5 days) | Unpaid with settlement 5 days out | | Any other | Success | Accepted response | ### Example: Test Unpaid Collection [#example-test-unpaid-collection] ```bash curl -X POST https://dev.api.revio.co.za/v2/mandate/eft/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "debtor": { "accountNumber": "110000002", ... }, ... }' ``` ## DebiCheck Mandate Test Scenarios [#debicheck-mandate-test-scenarios] Use specific contract references to simulate authentication outcomes: ### Contract Reference Scenarios [#contract-reference-scenarios] | Contract Reference | Scenario | Description | |---|---|---| | `NORESPONSE` | No response | Customer doesn't respond within 120 seconds | | `FAILINIT` | Init failure | Unexpected bank error during authentication | | `REJECT` | Rejected | Customer rejects the mandate | | `TIMEOUT` | Timeout | Authentication times out waiting for bank | | Any other | Success | Mandate authenticated successfully | ### Example: Test Rejected Mandate [#example-test-rejected-mandate] ```bash curl -X POST https://dev.api.revio.co.za/v2/mandate/debicheck/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "contractReference": "REJECT", "authenticationType": "REALTIME", ... }' ``` ### Example: Test No Response [#example-test-no-response] ```bash curl -X POST https://dev.api.revio.co.za/v2/mandate/debicheck/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "contractReference": "NORESPONSE", "authenticationType": "REALTIME", "fallbackAuthenticationType": "DELAYED_WITHOUT_AUTH", ... }' ``` ## Webhook Testing [#webhook-testing] ### Simulate Status Changes [#simulate-status-changes] Test callback handling by simulating status changes: ```bash curl -X POST https://dev.api.revio.co.za/v2/callback/test/collection/eft/{collectionId}/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "collectionStatus": "UNPAID", "callbackVersion": "V2" } ``` ### Test with Override URL [#test-with-override-url] Send callbacks to a test endpoint: ```bash curl -X POST https://dev.api.revio.co.za/v2/callback/test/collection/eft/{collectionId}/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "overrideUrl": "https://webhook.site/your-test-id", "collectionStatus": "COMPLETED", "callbackVersion": "V2" }' ``` ## Testing Checklist [#testing-checklist] ### Mandate Management [#mandate-management] - Create EFT mandate - Create DebiCheck mandate (REALTIME) - Create DebiCheck mandate (DELAYED) - Get mandate status - Amend mandate - Test authentication failure scenarios ### Collections [#collections] - Create EFT collection from the mandate - Create a DebiCheck collection from the mandate - Get collection status - Test unpaid scenarios - Test dispute scenarios ### Payments [#payments] - Create a same-day EFT payment - Create a one-day EFT payment - Create a two-day EFT payment - Get payment status ### Webhooks [#webhooks] - Register callback URL - Receive collection status callback - Receive mandate status callback - Handle all status types ### Error Handling [#error-handling] - Invalid authentication - Missing required fields - Invalid account numbers - Duplicate references ## DebiCheck Implementation Tips [#debicheck-implementation-tips] When implementing DebiCheck, consider: ### Client References [#client-references] Ensure each mandate has a unique contract reference (max 14 characters). This appears on customer bank statements alongside your abbreviated name. ### Timing [#timing] Plan for authentication outside business hours: - DebiCheck mandates can only be obtained 06:00-20:00 weekdays - Consider switching to TT2 (batch) for after-hours onboarding ### Managing Failures [#managing-failures] - Implement retry logic with `realtimeAuthRetries` - Use `fallbackAuthenticationType` for automatic failover - Follow up with customers who ignore authentication requests ### Mandate Rules [#mandate-rules] | Consideration | Guidance | |---|---| | Value Type | Choose `FIXED`, `VARIABLE`, or `USAGEBASED` based on billing model | | Amount Adjustments | Use `adjustmentRate` OR `adjustmentAmountCents`, not both | | Max Amount | Set `maxAmountCents` to accommodate your maximum expected collection | | Collection Day | Present collections for the authenticated day only | | Tracking | Allocate enough tracking days (max 10) for your retry strategy | ### Collection Timing [#collection-timing] - Submit collections at least 2 calendar days before the collection day - Do not adjust for public holidays or weekends (or use `allowDateAdjustment`) - Only collect on authenticated mandates ### Mandate Management [#mandate-management-1] Consider creating new mandates rather than amending existing ones to avoid complexity in re-authentication scenarios. ## Test Data [#test-data] ### Valid Test ID Numbers [#valid-test-id-numbers] Use a valid South African ID number format: - `0001010000006` - `8501015800089` - `0001010000089` ### Test Phone Numbers [#test-phone-numbers] Use format `+27-XXXXXXXXX`: - `+27-123456789` - `+27-615333440` - `+27-987654321` ### Test Bank Details [#test-bank-details] | Bank | Branch Code | |---|---| | ABSA | `632005`, `632500` | | Standard Bank | `051001` | | FNB | `250655` | | Nedbank | `198765` | | Capitec | `470010` | ## Moving to Production [#moving-to-production] Once testing is complete: 1. Review the Going Live checklist 2. Contact your Revio account manager for production credentials 3. Update your application configuration 4. Perform smoke tests in production Important: Never use production credentials in sandbox or vice versa. # Troubleshooting (/docs/direct-debit/troubleshooting) ## Authentication Issues [#authentication-issues] ### Invalid or Expired Token [#invalid-or-expired-token] **Symptom:**`401 Unauthorized` response **Causes:** - Access token has expired (tokens are valid for 24 hours) - Token not included in Authorization header - Malformed Authorization header **Solutions:** 1. Request a new access token: ```bash curl -X POST https://dev-payce.auth.eu-west-1.amazoncognito.com/oauth2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -d "grant_type=client_credentials" ``` 1. Ensure correct header format: ```text Authorization: Bearer eyJraWQiOiJ... ``` 1. Implement token refresh logic before expiry ### Invalid Client Credentials [#invalid-client-credentials] **Symptom:**`invalid_client` error from the authentication endpoint **Causes:** - Incorrect client_id or client_secret - Using sandbox credentials in production or vice versa - Credentials not Base64 encoded correctly **Solutions:** 1. Verify credentials match environment (sandbox vs production) 2. Check for extra whitespace in credentials 3. Ensure Basic auth header is properly formatted: ```bash # Correct format -u "client_id:client_secret" # Or manually encode echo -n "client_id:client_secret" | base64 ``` ## Mandate Creation Issues [#mandate-creation-issues] ### Bank Account Verification Failure [#bank-account-verification-failure] **Symptom:**`400 Bad Request` with verification failure details **Example Error:** ```json { "Message": "Debtor account verification failure: details={\"AccountExists\":true,\"AccountIsOpen\":false,...}" } ``` **Causes:** - Account is closed or dormant - Incorrect account number or branch code - Account type mismatch - ID number doesn't match the account holder **Solutions:** | Verification Field | Resolution | |---|---| | `AccountExists: false` | Verify account number with customer | | `AccountIsOpen: false` | Customer needs to reactivate or provide different account | | `AccountTypeMatches: false` | Correct the `accountType` field (CURRENT/SAVINGS/TRANSMISSION) | | `AccountAcceptsDebits: false` | Account cannot receive debits, use different account | | `IdNumberMatches: false` | Verify ID number matches primary account holder | **Workaround:** Set `verifyAccount: false` to skip verification (not recommended for production) ### Invalid Bank Name Format [#invalid-bank-name-format] **Symptom:**`400 Bad Request` mentioning the bank field **Cause:** Bank name not in the correct format **Solution:** Use uppercase, single-word format: | Incorrect | Correct | |---|---| | `Standard Bank` | `STANDARDBANK` | | `Absa` | `ABSA` | | `First National Bank` | `FNB` | | `capitec` | `CAPITEC` | ### Duplicate External Reference [#duplicate-external-reference] **Symptom:**`409 Conflict` error **Cause:** The `externalReference` value already exists **Solutions:** 1. Use unique references for each mandate/collection 2. Implement UUID generation: ```jsx const externalReference = crypto.randomUUID(); ``` 1. Include timestamp or sequence number: ```jsx const externalReference = `MAN-${Date.now()}-${sequenceNumber}`; ``` ### DebiCheck Authentication Timeout [#debicheck-authentication-timeout] **Symptom:** Mandate status remains `PENDING_AUTHENTICATION` then becomes `AUTH_FAILURE` **Causes:** - Customer didn't respond within 120 seconds (TT1) - The customer's phone is off or has no signal - The bank couldn't deliver USSD/push notification **Solutions:** 1. Implement fallback authentication: ```json { "authenticationType": "REALTIME", "fallbackAuthenticationType": "DELAYED_WITHOUT_AUTH" } ``` 1. Use delayed authentication for after-hours: ```json { "authenticationType": "DELAYED" } ``` 1. Retry with batch processing: ```json { "authenticationType": "BATCH" } ``` 1. Follow up with the customer to ensure they're expecting the authentication request ### DebiCheck Mandate Rejected [#debicheck-mandate-rejected] **Symptom:** Status `AUTH_FAILURE` with rejection reason **Causes:** - The customer actively rejected the mandate - Mandate details don't match customer expectations - The customer doesn't recognise the merchant **Solutions:** 1. Verify contract details with the customer before submitting 2. Ensure the abbreviated name is recognisable to the customer 3. Confirm collection amount and frequency 4. Re-submit with corrected details after customer confirmation ## Collection Issues [#collection-issues] ### Collection Returned Unpaid [#collection-returned-unpaid] **Symptom:** Collection status `UNPAID` **Common Causes and Solutions:** | Reason | Solution | |---|---| | Insufficient funds | Wait for next mandated date to retry | | Account closed | Contact customer for new account details | | Payment stopped | Contact customer, may need new mandate | | Not provided for | After 2 consecutive failures, obtain new mandate | **PASA Rules:** - Cannot resubmit until the next mandated date - After 2 "Not provided for" returns, a new mandate is required - "Account Closed" or "Payment Stopped" cannot be resubmitted ### Collection Disputed [#collection-disputed] **Symptom:** Collection status `DISPUTED` **Causes:** - Customer claims no valid mandate exists - Collection violates the mandate terms - The customer cancelled the mandate **Solutions:** 1. Retrieve and review the original mandate 2. Verify collection was within mandate parameters: - Correct amount (within max limits) - Correct collection day - The mandate was active 1. Respond to the bank within 30 days with the mandate evidence 2. For DebiCheck: Disputes are less likely if the collection matches the authenticated terms ### Collection Submitted After Cut-off [#collection-submitted-after-cut-off] **Symptom:** Collection not processed on the expected date **Cause:** Submitted after bank processing cut-off time **Cut-off Times:** | Service | Weekday | Weekend | |---|---|---| | Same-day | 16:20 | 09:45 | | One-day | 16:00 | 10:15 | | Two-day | 14:30 (2 days prior) | 09:00 | | DebiCheck | 16:00 (2 days prior) | 11:00 (2 days prior) | **Solutions:** 1. Submit collections well before cut-off times 2. Account for your system processing time 3. For time-sensitive collections, use same-day service with early submission ## Payment Issues [#payment-issues] ### Payment Exceeds Limits [#payment-exceeds-limits] **Symptom:** Payment rejected with "exceeds industry-imposed limits" **Cause:** Payment amount ≥ R5,000,000 **Solution:** Route high-value payments through a real-time line (contact Precium support) ### Payment to an Invalid Account [#payment-to-an-invalid-account] **Symptom:** Payment status `FAILED` **Causes:** - Invalid account number - Incorrect branch code - Account doesn't accept credits **Solutions:** 1. Verify recipient account details 2. Consider implementing account verification before payment 3. Confirm the branch code matches the account's bank ## Webhook Issues [#webhook-issues] ### Not Receiving Webhooks [#not-receiving-webhooks] **Symptom:** No callbacks received at registered URL **Causes:** - Callback URL not registered - URL not publicly accessible - HTTPS certificate issues - Firewall blocking requests **Solutions:** 1. Verify the callback URL is registered: ```bash # Register callback URL curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "clientId": YOUR_CLIENT_ID, "url": "https://your-server.com/webhook", "callbackType": "EFT_COLLECTION", "callbackVersion": "V2" }' ``` 1. Test with webhook.site first: ```json { "url": "https://webhook.site/your-unique-id" } ``` 1. Ensure your server: - Is publicly accessible - Has valid SSL certificate - Responds within 5 seconds - Returns HTTP 200 ### Webhook Timeout Errors [#webhook-timeout-errors] **Symptom:** Callbacks received but marked as failed **Cause:** Webhook handler takes too long to respond **Solution:** Return 200 immediately, process asynchronously: ```jsx app.post('/webhook', (req, res) => { // Respond immediately res.status(200).send('OK'); // Process in background processWebhookAsync(req.body); }); ``` ## API Request Issues [#api-request-issues] ### 400 Bad Request [#400-bad-request] **Symptom:** Request rejected with validation error **Common Causes:** 1. **Missing required fields** - Check all required fields are present - Verify field names match API specification (case-sensitive) 1. **Invalid date format** - Use ISO 8601 format: `2024-01-15T10:00:00.000Z` 1. **Invalid phone number format** - Use format: `+27-XXXXXXXXX` 1. **Invalid amount** - Amount must be in cents (integer) - `10000` = R100.00 1. **Invalid enum values** - Check accepted values for fields like `accountType`, `serviceType`, `frequency` ### 404 Not Found [#404-not-found] **Symptom:** Resource not found error **Causes:** - Invalid resource ID - Resource was deleted - Wrong endpoint URL **Solutions:** 1. Verify the ID exists and is correctly formatted (UUID) 2. Check endpoint URL matches documentation 3. Ensure you're using the correct API version (`/v2/`) ### 500 Internal Server Error [#500-internal-server-error] **Symptom:** Server error response **Actions:** 1. Note the timestamp and request details 2. Check for `x-request-id` in response headers 3. Retry with exponential backoff 4. If persistent, contact Precium support with: - Request endpoint - Request payload (redact sensitive data) - Timestamp - Request ID (if available) ## Environment Issues [#environment-issues] ### Using the Wrong Environment [#using-the-wrong-environment] **Symptom:** Unexpected behaviour, missing data, or authentication failures **Cause:** Mixing sandbox and production credentials/endpoints **Environment Reference:** | Environment | Auth Endpoint | API Endpoint | |---|---|---| | Sandbox | `dev-payce.auth.eu-west-1.amazoncognito.com` | `dev.api.revio.co.za` | | Production | `prod-payce.auth.eu-west-1.amazoncognito.com` | (provided at go-live) | **Solution:** Use environment variables to manage configuration: ```jsx const config = { authUrl: process.env.REVIO_AUTH_URL, apiUrl: process.env.REVIO_API_URL, clientId: process.env.REVIO_CLIENT_ID, clientSecret: process.env.REVIO_CLIENT_SECRET }; ``` ## Getting Help [#getting-help] If you cannot resolve an issue: 1. **Check documentation:** Review the relevant section of this documentation 2. **Review Swagger:** https://dev.api.revio.co.za/swagger/index.html 3. **Test in sandbox:** Reproduce the issue in the sandbox environment 4. **Contact support:** Email support@precium.com with: - Description of the issue - Steps to reproduce - Request/response examples (redact sensitive data) - Environment (sandbox/production) - Timestamp of failed requests *© 2026 Precium. All rights reserved.* # Webhooks (/docs/direct-debit/webhooks) Webhooks enable real-time notifications when transaction statuses change. ## Overview [#overview] The Revio API allows you to: - Configure webhook URLs for different event types - Receive real-time callbacks when collections or payments change status - Test webhook integrations in the sandbox environment There is no limit to the number of webhooks you can configure. ## Callback Types [#callback-types] | Type | Description | |---|---| | `EFT_COLLECTION` | EFT debit order collection status changes | | `DEBICHECK_COLLECTION` | DebiCheck collection status changes | | `DEBICHECK_MANDATE` | DebiCheck mandate status changes | | `EFT_PAYMENT` | EFT payment status changes | ## Step 1: Create Callback URL [step] [#step-1-create-callback-url-step] Register a URL to receive webhook notifications. ### Endpoint [step] [#endpoint-step] ```text POST https://api.revio.co.za/v2/clientcallbackurl/ ``` ### Request [step] [#request-step] ```bash curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clientId": 109, "url": "https://your-server.com/webhooks/revio", "callbackType": "EFT_COLLECTION", "callbackVersion": "V2" }' ``` ### Request Fields [step] [#request-fields-step] | Field | Type | Required | Description | |---|---|---|---| | `clientId` | integer | Yes | Your client ID (from merchant profile `createdByClient`) | | `url` | string | Yes | HTTPS URL to receive callbacks | | `callbackType` | string | Yes | Event type to subscribe to | | `callbackVersion` | string | Yes | Callback version (`V2` recommended) | ### Response [step] [#response-step] ```json { "id": "callback-url-123", "clientId": 109, "url": "https://your-server.com/webhooks/revio", "callbackType": "EFT_COLLECTION", "callbackVersion": "V2" } ``` ### Register Multiple Callback Types [step] [#register-multiple-callback-types-step] Create separate callback URLs for each event type: ```bash # EFT Collections [step] curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \ -d '{"clientId": 109, "url": "https://...", "callbackType": "EFT_COLLECTION", "callbackVersion": "V2"}' # DebiCheck Collections [step] curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \ -d '{"clientId": 109, "url": "https://...", "callbackType": "DEBICHECK_COLLECTION", "callbackVersion": "V2"}' # DebiCheck Mandates [step] curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \ -d '{"clientId": 109, "url": "https://...", "callbackType": "DEBICHECK_MANDATE", "callbackVersion": "V2"}' # EFT Payments [step] curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \ -d '{"clientId": 109, "url": "https://...", "callbackType": "EFT_PAYMENT", "callbackVersion": "V2"}' ``` ## Step 2: Trigger Callback Event [step] [#step-2-trigger-callback-event-step] Trigger a callback for a specific collection (useful for re-sending notifications). ### Endpoint [step] [#endpoint-step-1] ```text POST https://api.revio.co.za/v2/callback/collection/eft/{collectionId}/ ``` ### Request [step] [#request-step-1] ```bash curl -X POST https://dev.api.revio.co.za/v2/callback/collection/eft/63d94be3-1faf-428b-89f3-bc5271be8015/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "collectionStatus": "UNPAID", "callbackVersion": "V2" }' ``` This sends the current collection status to your configured callback URL. ## Step 3: Test Callback (Sandbox) [step] [#step-3-test-callback-sandbox-step] Simulate callback events to test your webhook handler without affecting actual transactions. ### Endpoint [step] [#endpoint-step-2] ```text POST https://api.revio.co.za/v2/callback/test/collection/eft/{collectionId}/ ``` ### Request [step] [#request-step-2] ```bash curl -X POST https://dev.api.revio.co.za/v2/callback/test/collection/eft/63d94be3-1faf-428b-89f3-bc5271be8015/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "collectionStatus": "UNPAID", "callbackVersion": "V2" }' ``` ### Request Fields [step] [#request-fields-step-1] | Field | Type | Required | Description | |---|---|---|---| | `collectionStatus` | string | Yes | Status to simulate | | `callbackVersion` | string | Yes | Callback version | | `overrideUrl` | string | No | Send to different URL for testing | ### Override URL Example [step] [#override-url-example-step] Send test callback to a different URL: ```json { "overrideUrl": "https://webhook.site/your-test-id", "collectionStatus": "COMPLETED", "callbackVersion": "V2" } ``` ## Callback Payload [#callback-payload] When a status change occurs, Revio sends a POST request to your webhook URL. ### EFT Collection Callback [#eft-collection-callback] ```json { "id": "63d94be3-1faf-428b-89f3-bc5271be8015", "type": "EFT_COLLECTION", "status": "COMPLETED", "mandateId": "8f4a6b0f-fa9b-4f45-8e19-b75c1a70432a", "contractReference": "CONTRACT001", "amountCents": 10000, "collectionDate": "2024-02-15T00:00:00.000Z", "statementId": "stmt_12345", "timestamp": "2024-02-15T14:30:00.000Z" } ``` ### DebiCheck Mandate Callback [#debicheck-mandate-callback] ```json { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "DEBICHECK_MANDATE", "status": "COMPLETED", "contractReference": "DC001", "authenticationType": "REALTIME", "timestamp": "2024-02-15T14:30:00.000Z" } ``` ### DebiCheck Collection Callback [#debicheck-collection-callback] ```json { "id": "coll-123-456-789", "type": "DEBICHECK_COLLECTION", "status": "COMPLETED", "mandateId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "amountCents": 10000, "timestamp": "2024-02-15T14:30:00.000Z" } ``` ## Handling Webhooks [#handling-webhooks] ### Webhook Handler Example (Node.js) [#webhook-handler-example-nodejs] ```jsx const express = require('express'); const app = express(); app.use(express.json()); app.post('/webhooks/revio', (req, res) => { const event = req.body; console.log('Received webhook:', event.type, event.status); switch (event.type) { case 'EFT_COLLECTION': handleEftCollection(event); break; case 'DEBICHECK_COLLECTION': handleDebicheckCollection(event); break; case 'DEBICHECK_MANDATE': handleDebicheckMandate(event); break; case 'EFT_PAYMENT': handleEftPayment(event); break; } // Always respond with 200 OK res.status(200).send('OK'); }); function handleEftCollection(event) { if (event.status === 'COMPLETED') { // Mark payment as successful in your system } else if (event.status === 'UNPAID') { // Handle failed collection } } ``` ### Best Practices [#best-practices] 1. **Respond quickly**: Return 200 OK within 5 seconds 2. **Process asynchronously**: Queue events for background processing 3. **Handle duplicates**: Implement idempotency using the event ID 4. **Do not trust the payload**: callbacks are unsigned, so re-read state from the relevant `/status` endpoint before acting — see *Webhook Security* below 5. **Log everything**: Keep detailed logs for debugging 6. **Retry handling**: Handle the same event being sent multiple times ## Webhook Security [#webhook-security] **Callbacks on this API are not signed.** There is no signature header and no public key, so a callback cannot be authenticated from its content. Anyone who learns your callback URL can post to it. Design on that basis: 1. **Treat the payload as a prompt, not as truth.** On receiving a callback, call the relevant `/status` endpoint — `/v2/mandate/{type}/{id}/status`, `/v2/collection/{type}/{id}/status` or `/v2/payment/eft/{id}/status` — and act on the authoritative response. Never move money or change state on the basis of callback values alone. 2. **Keep the URL unguessable** and treat it as a secret: a long random path segment, rotated if it is ever exposed in a log or a support ticket. 3. **Reconcile independently.** The `/v2/event/*` endpoints are polled by you over an authenticated connection, so they are the trustworthy basis for reconciliation. The card APIs *do* sign their callbacks, with an `X-Signature` header over an RSA signature. That mechanism does not exist here — do not carry the assumption across between the two. ### URL Requirements [#url-requirements] - Must use HTTPS - Must be publicly accessible - Should respond within 5 seconds ### Recommendations [#recommendations] 1. **Use a unique path**: Include a secret in your webhook URL 2. **Validate payloads**: Check expected fields are present 3. **Monitor failures**: Alert on repeated delivery failures ## Testing Tools [#testing-tools] Use these services to test webhooks during development: - [Webhook.site](https://webhook.site/) - Free webhook testing - [RequestBin](https://requestbin.com/) - Inspect webhook payloads - [ngrok](https://ngrok.com/) - Expose local servers ### Example with Webhook.site [#example-with-webhooksite] 1. Go to webhook.site and copy your unique URL 2. Register it as your callback URL: ```bash curl -X POST https://dev.api.revio.co.za/v2/clientcallbackurl/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clientId": 109, "url": "https://webhook.site/your-unique-id", "callbackType": "EFT_COLLECTION", "callbackVersion": "V2" }' ``` 1. Trigger a test callback to see the payload ## Troubleshooting [#troubleshooting] ### Common Issues [#common-issues] | Issue | Cause | Solution | |---|---|---| | No callbacks received | URL not registered | Verify callback URL configuration | | 404 errors | Wrong endpoint | Check your webhook handler path | | Timeout errors | Slow processing | Return 200 immediately, process async | | Missing events | Wrong callback type | Register for correct event types | ### Verify Callback Registration [#verify-callback-registration] Get your client ID from merchant profile: ```bash curl -X GET https://dev.api.revio.co.za/v2/merchantprofile/ \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` The `createdByClient` field is your client ID for webhook registration. # Hello World (/docs) Welcome to the docs! You can start writing documents in `/content/docs`. ## What is Next? [#what-is-next] # Precium API Documentation (/docs/overview) Precium exposes two systems. Which one you integrate against depends on what you are collecting, not on which product name you were sold. ## Products [#products] }> Take card data on your own server and post it directly. The most control and the widest PCI obligation: SAQ D. }> Hosted Checkout, Payment Links and Direct Post over the same API, with the PCI obligation reduced to SAQ A or SAQ A-EP. }> DebiCheck and EFT collections, mandate lifecycle and settlement reporting. ## One API, two products [#one-api-two-products] Server-to-Server and Payment Orchestration are **two documentation sets over one API**. Both call the card gateway, and both resolve against the same specification, which is why there is a single [card API reference](/docs/reference/card) rather than one per product. The **Direct Debit order API is a separate system on its own base URL**, with its own authentication. Its different path convention is by design, not an inconsistency. Easy to miss and worth stating plainly: **debit order payment methods also appear in the ordinary checkout flow**, as `debicheck` and `eft` in `payment_method_whitelist` on a normal purchase. So debit orders have a shallow route through orchestration and a full mandate lifecycle route through the Direct Debit API, and they are not the same integration. ## Reference [#reference] }> Every operation on the card gateway, generated from the specification. }> Every operation on the Mandates API, generated from the specification. # Code examples (/docs/payment-orchestration/code-examples) Production-ready code examples in multiple programming languages. ## Configuration [#configuration] ```python import requests from typing import Optional, Dict, Any, List class PreciumClient: def __init__(self, api_key: str, brand_id: str): self.api_key = api_key self.brand_id = brand_id self.base_url = "https://gate.reviopay.com/api/v1" self.session = requests.Session() self.session.headers.update({ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" }) def _request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]: url = f"{self.base_url}{endpoint}" response = self.session.request(method, url, json=data) response.raise_for_status() return response.json() if response.text else {} # Initialize precium = PreciumClient("your_api_key", "your_brand_id") ``` ```jsx const axios = require('axios'); class PreciumClient { constructor(apiKey, brandId) { this.brandId = brandId; this.client = axios.create({ baseURL: 'https://gate.reviopay.com/api/v1', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } }); } async request(method, endpoint, data = null) { const response = await this.client.request({ method, url: endpoint, data }); return response.data; } } const precium = new PreciumClient('your_api_key', 'your_brand_id'); module.exports = precium; ``` ```php apiKey = $apiKey; $this->brandId = $brandId; } public function request(string $method, string $endpoint, ?array $data = null): array { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $this->baseUrl . $endpoint, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => strtoupper($method), CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $this->apiKey, 'Content-Type: application/json' ] ]); if ($data !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } $response = curl_exec($ch); curl_close($ch); return json_decode($response, true) ?? []; } } $precium = new PreciumClient('your_api_key', 'your_brand_id'); ``` ## Payment Link [#payment-link] **Create Payment Link** ```python def create_payment_link(email: str, amount_cents: int, product_name: str, reference: str = None) -> Dict: data = { "client": {"email": email}, "purchase": { "currency": "ZAR", "products": [{"name": product_name, "price": amount_cents}] }, "brand_id": precium.brand_id } if reference: data["reference"] = reference return precium._request("POST", "/purchases/", data) # Usage payment = create_payment_link( email='customer@example.com', amount_cents=150000, product_name='Invoice #12345' ) print(f"Payment link: {payment['checkout_url']}") ``` ```jsx async function createPaymentLink(email, amountCents, productName, reference = null) { const data = { client: { email }, purchase: { currency: 'ZAR', products: [{ name: productName, price: amountCents }] }, brand_id: precium.brandId }; if (reference) data.reference = reference; return await precium.request('POST', '/purchases/', data); } // Usage const payment = await createPaymentLink('customer@example.com', 150000, 'Invoice #12345'); console.log(`Payment link: ${payment.checkout_url}`); ``` ## E-commerce Checkout [#e-commerce-checkout] **Create Checkout with Redirects** ```python def create_checkout( client_id: str, products: List[Dict], order_id: str, success_url: str, failure_url: str, payment_methods: List[str] = None ) -> Dict: data = { "client_id": client_id, "purchase": {"currency": "ZAR", "products": products}, "brand_id": precium.brand_id, "reference": order_id, "success_redirect": success_url, "failure_redirect": failure_url } if payment_methods: data["payment_method_whitelist"] = payment_methods return precium._request("POST", "/purchases/", data) # Usage checkout = create_checkout( client_id='client-uuid', products=[ {"name": "Widget", "price": 5000, "quantity": "2"}, {"name": "Shipping", "price": 5000} ], order_id='ORD-2026-0001', success_url='https://shop.com/success', failure_url='https://shop.com/failure', payment_methods=["visa", "mastercard", "ozow"] ) ``` ```jsx async function createCheckout(clientId, products, orderId, successUrl, failureUrl, paymentMethods = null) { const data = { client_id: clientId, purchase: { currency: 'ZAR', products }, brand_id: precium.brandId, reference: orderId, success_redirect: successUrl, failure_redirect: failureUrl }; if (paymentMethods) data.payment_method_whitelist = paymentMethods; return await precium.request('POST', '/purchases/', data); } ``` ## Recurring Payments [#recurring-payments] **Tokenize Card (First Payment)** **Python:** ```python def create_tokenization_purchase( client_id: str, amount_cents: int, product_name: str, success_url: str, failure_url: str ) -> Dict: return precium._request("POST", "/purchases/", { "client_id": client_id, "purchase": { "currency": "ZAR", "products": [{"name": product_name, "price": amount_cents}] }, "brand_id": precium.brand_id, "force_recurring": True, "success_redirect": success_url, "failure_redirect": failure_url }) # Store the returned purchase ID as your recurring token ``` **Charge with Token** **Python:** ```python def charge_recurring( client_id: str, recurring_token: str, amount_cents: int, product_name: str, reference: str ) -> Dict: # Create purchase purchase = precium._request("POST", "/purchases/", { "client_id": client_id, "purchase": { "currency": "ZAR", "products": [{"name": product_name, "price": amount_cents}] }, "brand_id": precium.brand_id, "reference": reference }) # Charge using token return precium._request( "POST", f"/purchases/{purchase['id']}/charge/", {"recurring_token": recurring_token} ) ``` ## Refunds [#refunds] ```python def refund_full(purchase_id: str) -> Dict: return precium._request("POST", f"/purchases/{purchase_id}/refund/", {}) def refund_partial(purchase_id: str, amount_cents: int) -> Dict: return precium._request("POST", f"/purchases/{purchase_id}/refund/", {"amount": amount_cents}) ``` ```jsx async function refund(purchaseId, amountCents = null) { const data = amountCents ? { amount: amountCents } : {}; return await precium.request('POST', `/purchases/${purchaseId}/refund/`, data); } ``` ## Webhooks [#webhooks] ```jsx const crypto = require('crypto'); // Callbacks are signed with ASYMMETRIC RSA — there is no shared secret. // X-Signature = base64( RSA PKCS#1 v1.5 over the SHA-256 digest of the raw body ) // Key source: public_key on the webhook object, or GET /public_key/ for a // per-purchase success_callback. Store the PEM, do not fetch it per request. const PUBLIC_KEY_PEM = process.env.PRECIUM_WEBHOOK_PUBLIC_KEY; function verifySignature(rawBody, signatureB64) { const verifier = crypto.createVerify('RSA-SHA256'); verifier.update(rawBody); // raw Buffer, not a re-serialised object verifier.end(); return verifier.verify(PUBLIC_KEY_PEM, Buffer.from(signatureB64, 'base64')); } // express.raw is essential: JSON.parse then re-stringify breaks the signature. app.post('/webhooks/precium', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-signature']; if (!signature || !verifySignature(req.body, signature)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body.toString()); // Deliveries can repeat even after a 200, so make this idempotent. switch (event.event_type) { case 'purchase.paid': handlePaymentSuccess(event.data); break; case 'purchase.payment_failure': handlePaymentFailure(event.data); break; case 'payment.refunded': handleRefund(event.data); break; } res.status(200).send('OK'); // respond within 5 seconds; queue the work }); ``` ```python import base64 import os from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding from cryptography.exceptions import InvalidSignature from flask import Flask, request app = Flask(__name__) # Callbacks are signed with ASYMMETRIC RSA — there is no shared secret. # X-Signature = base64( RSA PKCS#1 v1.5 over the SHA-256 digest of the raw body ) # Key source: public_key on the webhook object, or GET /public_key/ for a # per-purchase success_callback. PUBLIC_KEY = serialization.load_pem_public_key( os.environ["PRECIUM_WEBHOOK_PUBLIC_KEY"].encode() ) def verify_signature(raw_body: bytes, signature_b64: str) -> bool: try: PUBLIC_KEY.verify( base64.b64decode(signature_b64), raw_body, # raw bytes, never request.get_json() padding.PKCS1v15(), hashes.SHA256(), ) return True except (InvalidSignature, ValueError): return False @app.route('/webhooks/precium', methods=['POST']) def handle_webhook(): signature = request.headers.get('X-Signature', '') raw_body = request.get_data() # bytes, before any parsing if not verify_signature(raw_body, signature): return 'Invalid signature', 401 event = request.get_json() # Deliveries can repeat even after a 200, so make this idempotent. if event['event_type'] == 'purchase.paid': handle_payment_success(event['data']) elif event['event_type'] == 'purchase.payment_failure': handle_payment_failure(event['data']) elif event['event_type'] == 'payment.refunded': handle_refund(event['data']) return 'OK', 200 # respond within 5 seconds ``` ## Direct Post Form [#direct-post-form] ```html
``` ## Subscriptions [#subscriptions] **Create Plan** ```python def create_subscription_plan( title: str, price_cents: int, period: int = 1, period_units: str = "months" ) -> Dict: return precium._request("POST", "/billing_templates/", { "title": title, "purchase": { "currency": "ZAR", "products": [{"name": title, "price": price_cents}] }, "brand_id": precium.brand_id, "is_subscription": True, "subscription_active": True, "subscription_period": period, "subscription_period_units": period_units, "force_recurring": True }) ``` **Add Subscriber** ```python def add_subscriber(template_id: str, client_id: str, reference: str = None) -> Dict: data = {"client_id": client_id, "send_receipt": True} if reference: data["invoice_reference"] = reference return precium._request( "POST", f"/billing_templates/{template_id}/add_subscriber/", data ) ``` ## Error Handling [#error-handling] ```python def safe_api_call(func, *args, **kwargs): try: return func(*args, **kwargs) except requests.exceptions.HTTPError as e: if e.response.status_code == 401: print("Authentication failed - check API key") elif e.response.status_code == 400: errors = e.response.json() print(f"Validation error: {errors}") elif e.response.status_code == 429: print("Rate limited - implement backoff") else: print(f"API error: {e}") return None ``` # Error mapping (/docs/payment-orchestration/error-mapping) ## Precium API Error Reference [#precium-api-error-reference] This document provides a comprehensive reference for all error codes, their causes, and recommended resolution steps. ## Error Response Format [#error-response-format] All API errors follow a consistent JSON structure: ```json { "__all__": { "message": "Descriptive error message", "code": "error_code" } } ``` **Field-specific errors:** ```json { "email": { "message": "Enter a valid email address", "code": "invalid" }, "purchase.currency": { "message": "This field is required", "code": "required" } } ``` ## HTTP Status Codes [#http-status-codes] | Status | Description | Common Causes | |---|---|---| | `200` | Success | Request completed successfully | | `201` | Created | Resource created successfully | | `204` | No Content | Successful deletion | | `400` | Bad Request | Validation errors, malformed JSON | | `401` | Unauthorized | Invalid or missing API key | | `403` | Forbidden | Insufficient permissions | | `404` | Not Found | Resource doesn't exist | | `409` | Conflict | Duplicate request, state conflict | | `422` | Unprocessable Entity | Business logic error | | `429` | Too Many Requests | Rate limit exceeded | | `500` | Internal Server Error | Server-side error | | `502` | Bad Gateway | Upstream service unavailable | | `503` | Service Unavailable | Temporary maintenance | ## Authentication Errors [#authentication-errors] **`invalid_token`** **HTTP Status:** 401 **Message:** Invalid or expired authentication token **Causes:** - API key is incorrect - API key has been revoked - Using sandbox key in production or vice versa **Resolution:** 1. Verify your API key in the dashboard 2. Ensure you're using the correct environment 3. Generate a new API key if necessary **`missing_authorization`** **HTTP Status:** 401 **Message:** Authorization header is required **Causes:** - No Authorization header provided - Malformed Authorization header **Resolution:** ```bash # Correct format -H "Authorization: Bearer YOUR_API_KEY" # Common mistakes -H "Authorization: YOUR_API_KEY" # Missing "Bearer" -H "Bearer YOUR_API_KEY" # Missing "Authorization:" ``` ## Validation Errors [#validation-errors] **`required`** **HTTP Status:** 400 **Message:** This field is required **Example:** ```json { "brand_id": { "message": "This field is required", "code": "required" } } ``` **Resolution:** Include all required fields in your request. **`invalid`** **HTTP Status:** 400 **Message:** Invalid value for this field **Common Cases:** | Field | Valid Format | Example Error | |---|---|---| | `email` | Valid email | `invalid_email@` | | `currency` | ISO 4217 (3 chars) | `RAND` instead of `ZAR` | | `phone` | E.164 format | `0821234567` instead of `+27821234567` | | `country` | ISO 3166-1 alpha-2 | `South Africa` instead of `ZA` | **`invalid_uuid`** **HTTP Status:** 400 **Message:** Invalid UUID format **Causes:** - `client_id` or `brand_id` is not a valid UUID - UUID contains invalid characters **Valid UUID format:** ```text 3fa85f64-5717-4562-b3fc-2c963f66afa6 ``` **`max_length`** **HTTP Status:** 400 **Message:** Ensure this field has no more than X characters **Field Limits:** | Field | Max Length | |---|---| | `reference` | 128 | | `purchase.products[].name` | 256 | | `success_redirect` | 500 | | `cardholder_name` | 30 | | `card_number` | 19 | **`min_value` / `max_value`** **HTTP Status:** 400 **Message:** Ensure this value is greater/less than X **Amount Limits:** | Field | Min | Max | |---|---|---| | `purchase.products[].price` | 0 | 9,999,999,999 | | `purchase.total_override` | 0 | 999,999,999,999,999 | ## Purchase Errors [#purchase-errors] **`purchase_not_found`** **HTTP Status:** 404 **Message:** Purchase not found **Causes:** - Invalid purchase ID - The purchase belongs to a different merchant - Purchase has been deleted **Resolution:** 1. Verify the purchase ID is correct 2. Ensure you're using the correct API key **`purchase_already_paid`** **HTTP Status:** 409 **Message:** Purchase has already been paid **Causes:** - Attempting to charge an already-completed purchase - Duplicate payment submission **Resolution:** - Check purchase status before charging - Implement idempotency in your integration **`purchase_cancelled`** **HTTP Status:** 409 **Message:** Purchase has been cancelled **Causes:** - Attempting to charge a cancelled purchase - Purchase expired and was auto-cancelled **Resolution:** - Create a new purchase for the transaction **`purchase_expired`** **HTTP Status:** 409 **Message:** Purchase has expired **Causes:** - `due` the date has passed with `due_strict: true` - Direct post URL has expired (15-minute validity) **Resolution:** - Create a new purchase - Process payments promptly after creation **`invalid_amount`** **HTTP Status:** 400 **Message:** Invalid amount specified **Causes:** - Negative amount - Amount exceeds original purchase - Zero amount for non-authorization purchase **Resolution:** - Ensure the amount is positive - For partial captures/refunds, ensure the amount ≤ remaining **`capture_not_allowed`** **HTTP Status:** 409 **Message:** Capture is not allowed for this purchase **Causes:** - Purchase was not created with `skip_capture: true` - Purchase is not in `hold` status - Authorization has expired **Resolution:** - Verify the purchase status is `hold` - Create a new purchase with `skip_capture: true` for pre-auth **`refund_exceeds_amount`** **HTTP Status:** 400 **Message:** Refund amount exceeds available balance **Causes:** - Refund amount greater than the original payment - Previous refunds have reduced the available balance **Resolution:** ```python # Check available refund amount purchase = get_purchase(purchase_id) available = purchase['purchase']['total'] - purchase.get('refunded_amount', 0) ``` ## Payment Method Errors [#payment-method-errors] **`payment_method_not_available`** **HTTP Status:** 400 **Message:** Payment method not available **Causes:** - Payment method not enabled for your merchant - Payment method not supported for the currency - Payment method excluded by `payment_method_whitelist` **Resolution:** 1. Check the enabled payment methods 2. Verify currency support for the payment method 3. Review `payment_method_whitelist` in request **`recurring_token_not_found`** **HTTP Status:** 404 **Message:** Recurring token not found **Causes:** - Invalid token ID - Token has been deleted - The token belongs to a different client **Resolution:** 1. List client tokens: `GET /clients/{id}/recurring_tokens/` 2. Verify the token ID is correct 3. Re-tokenize if necessary **`recurring_token_expired`** **HTTP Status:** 409 **Message:** Recurring token has expired **Causes:** - The card has expired - Token validity period exceeded - Mandate expired (DebiCheck) **Resolution:** - Request the customer to re-tokenize payment method **`card_declined`** **HTTP Status:** 422 **Message:** Card was declined **Decline Reasons:** | Code | Description | Customer Action | |---|---|---| | `do_not_honour` | Generic decline | Try different card | | `insufficient_funds` | Not enough balance | Add funds or use different card | | `expired_card` | Card has expired | Use valid card | | `invalid_card` | Card number invalid | Check card details | | `lost_stolen` | Card reported lost/stolen | Contact bank | | `suspected_fraud` | Fraud suspicion | Contact bank | **`3ds_authentication_failed`** **HTTP Status:** 422 **Message:** 3D Secure authentication failed **Causes:** - Customer failed 3DS challenge - 3DS timeout - Customer cancelled authentication **Resolution:** - The customer should retry with the correct credentials - Ensure the 3DS page loads correctly ## Client Errors [#client-errors] **`client_not_found`** **HTTP Status:** 404 **Message:** Client not found **Resolution:** 1. Verify client ID is correct 2. Create a client if it doesn't exist `duplicate_client` **HTTP Status:** 409 **Message:** Client with this email already exists **Causes:** - Attempting to create a client with an existing email **Resolution:** - Search for existing client: `GET /clients/?email=customer@example.com` - Use existing client ID ## Billing Template Errors [#billing-template-errors] **`billing_template_not_found`** **HTTP Status:** 404 **Message:** Billing template not found **`subscriber_already_exists`** **HTTP Status:** 409 **Message:** Client is already subscribed to this template **Resolution:** - Check existing subscription status - Update existing subscription instead of creating new one **`subscription_inactive`** **HTTP Status:** 409 **Message:** Subscription is not active **Causes:** - `subscription_active: false` on template - Subscription has been cancelled ## Webhook Errors [#webhook-errors] **`webhook_url_invalid`** **HTTP Status:** 400 **Message:** Invalid webhook URL **Requirements:** - Must be HTTPS - Must be publicly accessible - Must respond within 5 seconds **`webhook_url_unreachable`** **HTTP Status:** 400 **Message:** Webhook URL is not reachable **Resolution:** 1. Verify URL is publicly accessible 2. Check firewall rules 3. Ensure the SSL certificate is valid ## Rate Limiting Errors [#rate-limiting-errors] **`rate_limit_exceeded`** **HTTP Status:** 429 **Message:** Rate limit exceeded **Response Headers:** ```text X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1619740860 ``` **Resolution:** 1. Implement exponential backoff 2. Cache responses where possible 3. Batch operations when available **Retry Logic:** ```python import time def api_request_with_retry(func, max_retries=3): for attempt in range(max_retries): response = func() if response.status_code != 429: return response reset_time = int(response.headers.get('X-RateLimit-Reset', 0)) wait_time = max(reset_time - time.time(), 2 ** attempt) time.sleep(wait_time) raise Exception("Rate limit exceeded after retries") ``` ## Direct Post Errors [#direct-post-errors] **`direct_post_expired`** **HTTP Status:** 400 **Message:** Direct post URL has expired **Causes:** - URL validity is 15 minutes - URL has already been used **Resolution:** - Create a new purchase for the fresh direct post URL **`invalid_card_data`** **HTTP Status:** 400 **Message:** Invalid card data submitted **Field Validation:** | Field | Validation | Error | |---|---|---| | `cardholder_name` | Latin letters, space, apostrophe, dot, dash | Non-Latin characters | | `card_number` | Digits only, valid Luhn | Invalid format | | `expires` | MM/YY format, future date | Past date or invalid format | | `cvc` | 3-4 digits | Invalid length | ## Error Handling Best Practices [#error-handling-best-practices] **1. Always Check HTTP Status** ```python response = requests.post(url, json=data, headers=headers) if response.status_code == 201: # Success - process response purchase = response.json() elif response.status_code == 400: # Validation error - show user-friendly message errors = response.json() handle_validation_errors(errors) elif response.status_code == 401: # Auth error - check credentials refresh_credentials() elif response.status_code == 429: # Rate limited - retry with backoff retry_with_backoff() else: # Other error - log and alert log_error(response) ``` **2. Display User-Friendly Messages** ```python ERROR_MESSAGES = { 'card_declined': 'Your card was declined. Please try a different payment method.', 'insufficient_funds': 'Insufficient funds. Please use a different card.', 'expired_card': 'Your card has expired. Please use a valid card.', '3ds_authentication_failed': 'Authentication failed. Please try again.', 'invalid': 'Please check your information and try again.' } def get_user_message(error_code): return ERROR_MESSAGES.get(error_code, 'An error occurred. Please try again.') ``` **3. Log Errors for Debugging** ```python import logging def log_api_error(response, request_data): logging.error( "API Error", extra={ 'status_code': response.status_code, 'response_body': response.text, 'request_url': response.url, 'request_data': sanitize_sensitive_data(request_data) } ) ``` **4. Implement Retry Logic** ```python from tenacity import retry, stop_after_attempt, wait_exponential @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), retry=lambda e: isinstance(e, RetryableError) ) def make_api_request(): response = requests.post(...) if response.status_code in [429, 502, 503]: raise RetryableError() return response ``` ## Error Code Quick Reference [#error-code-quick-reference] | Code | HTTP | Category | Action | |---|---|---|---| | `invalid_token` | 401 | Auth | Check API key | | `required` | 400 | Validation | Add missing field | | `invalid` | 400 | Validation | Fix field format | | `purchase_not_found` | 404 | Resource | Verify ID | | `purchase_already_paid` | 409 | State | Check status first | | `card_declined` | 422 | Payment | Try different card | | `insufficient_funds` | 422 | Payment | Try different card | | `3ds_authentication_failed` | 422 | Payment | Retry authentication | | `recurring_token_expired` | 409 | Token | Re-tokenize | | `rate_limit_exceeded` | 429 | Rate Limit | Implement backoff | # Glossary (/docs/payment-orchestration/glossary) Definitions of payment industry terms and Precium-specific concepts. ## A [#a] ### 3D Secure (3DS) [#3d-secure-3ds] An authentication protocol that adds an extra layer of security to online card transactions. Cardholders verify their identity with their bank during checkout, typically via OTP or biometric authentication. Precium supports 3DS 2.0. ### Acquirer / Acquiring Bank [#acquirer--acquiring-bank] The financial institution that processes card payments on behalf of a merchant. The acquirer receives transaction requests and communicates with card networks to authorize and settle payments. ### API Key [#api-key] A unique identifier used to authenticate API requests. Precium provides separate API keys for sandbox and production environments. ### Authorization [#authorization] The process of verifying that a cardholder has sufficient funds or credit for a transaction. An authorization does not transfer funds, it reserves them for later capture. ### Authorization Code (Auth Code) [#authorization-code-auth-code] A code returned by the card issuer confirming that a transaction has been approved. Used as proof of authorization. ### AVS (Address Verification Service) [#avs-address-verification-service] A fraud prevention measure that compares the billing address provided during checkout with the address on file with the card issuer. ## B [#b] ### Billing Template [#billing-template] A Precium resource that defines recurring billing configuration, including pricing, frequency, and payment method requirements. Used to automate subscription billing. ### Brand [#brand] A merchant configuration in Precium that includes branding (logo, colours), payment method settings, and API credentials. Merchants can have multiple brands. ### Brand ID [#brand-id] The unique identifier for a brand configuration. Required in most API requests to identify which merchant configuration to use. ## C [#c] ### Cancel [#cancel] To void a purchase before payment is completed or captured. Cancelled purchases cannot be paid. ### Capture [#capture] The process of completing a previously authorized transaction and initiating a fund transfer. Used in two-step payment flows (authorize then capture). ### Card Network [#card-network] Organisations that facilitate card payments between issuers and acquirers (e.g., Visa, Mastercard). They set rules, process transactions, and manage settlement. ### Card Not Present (CNP) [#card-not-present-cnp] A transaction where the physical card is not presented, such as online or phone payments. CNP transactions have different security requirements than in-person payments. ### Card Scheme [#card-scheme] See Card Network. ### Cardholder [#cardholder] The person whose name appears on a payment card and who is authorized to use it. ### Checkout URL [#checkout-url] A URL provided by Precium that redirects customers to a hosted payment page. The safest integration method for PCI compliance. ### Client [#client] A customer record in Precium contains contact information, billing address, and stored payment methods. ### CVC/CVV [#cvccvv] Card Verification Code/Value. The 3 or 4-digit security code on a payment card is used to verify card-not-present transactions. ## D [#d] ### DebiCheck [#debicheck] A South African authenticated debit order system. Customers authorize mandates via their banking app, providing greater security than traditional debit orders. ### Debit Order [#debit-order] A payment instruction that allows a merchant to collect funds directly from a customer's bank account. Traditional debit orders don't require real-time authentication. ### Direct Post [#direct-post] An integration method where card data is submitted directly from the customer's browser to Precium, bypassing the merchant's servers. Requires PCI SAQ A-EP compliance. ### Dispute [#dispute] A challenge to a payment initiated by a cardholder through their bank. Also known as a chargeback. ## E [#e] ### EFT (Electronic Funds Transfer) [#eft-electronic-funds-transfer] The electronic movement of money between bank accounts. Includes methods like Ozow, Stitch, and traditional bank transfers. ## F [#f] ### Failure Redirect [#failure-redirect] The URL where customers are redirected after a failed payment attempt. ### Force Recurring [#force-recurring] A flag that ensures payment credentials are tokenized for future use, even if the customer doesn't opt in. ## G [#g] ### Gateway [#gateway] A service that transmits payment data between merchants and payment processors. Precium acts as a payment gateway. ## H [#h] ### Hold [#hold] A temporary reservation of funds on a card, typically used for pre-authorization. The hold reserves funds without completing the transaction. ### Hosted Checkout [#hosted-checkout] A payment page hosted by Precium where customers enter payment details. Provides the highest level of PCI compliance simplification. ## I [#i] ### Idempotency [#idempotency] The property of an operation that, when performed multiple times, produces the same result. Important for retry logic to prevent duplicate charges. ### Instant EFT [#instant-eft] Electronic funds transfer methods that provide immediate or near-immediate payment confirmation (e.g., Ozow, Capitec Pay). ### Issuer / Issuing Bank [#issuer--issuing-bank] The bank that issues payment cards to consumers and approves or declines transactions. ## L [#l] ### Live / Production [#live--production] The real payment environment where actual funds are transferred. Contrasted with sandbox/test environments. ## M [#m] ### Mandate [#mandate] An authorization from a customer to collect payments from their account. Used in debit order and DebiCheck scenarios. ### Merchant [#merchant] A business that accepts payments for goods or services. ### Merchant ID (MID) [#merchant-id-mid] A unique identifier assigned to a merchant by their acquirer or payment processor. ### MIT (Merchant Initiated Transaction) [#mit-merchant-initiated-transaction] A transaction initiated by the merchant without the cardholder being present, such as recurring billing or scheduled payments. ## O [#o] ### Orchestration [#orchestration] The coordination of multiple payment providers, methods, and flows through a single platform. Precium provides payment orchestration. ## P [#p] ### Payment Link [#payment-link] A URL that directs customers to a payment page for a specific transaction. Can be shared via email, SMS, or messaging apps. ### Payment Method [#payment-method] The specific instrument used for payment (e.g., Visa, Mastercard, Ozow, DebiCheck). ### Payment Method Whitelist [#payment-method-whitelist] A list of payment methods that customers are allowed to use for a specific purchase. ### PCI DSS [#pci-dss] Payment Card Industry Data Security Standard. A set of security requirements for organisations that handle card data. ### PCI Scope [#pci-scope] The extent of PCI DSS requirements applicable to an organization based on how they handle card data. ### Pre-Authorization [#pre-authorization] An authorization that reserves funds without immediate capture. Used when the final amount may differ from the initial authorization (e.g., hotels, car rentals). ### Purchase [#purchase] A payment request in Precium. Contains product information, pricing, and payment configuration. ### Purchase ID [#purchase-id] The unique identifier for a purchase transaction. ## R [#r] ### Recurring Payment [#recurring-payment] A payment that occurs on a regular schedule using stored payment credentials. ### Recurring Token [#recurring-token] A secure reference to stored payment credentials that can be used to process future payments without re-entering details. ### Refund [#refund] The return of funds to a customer after a payment is completed. Can be full or partial. ### Release [#release] The action of removing a hold on funds without capturing them. ### RRN (Retrieval Reference Number) [#rrn-retrieval-reference-number] A unique identifier assigned to a transaction that can be used to trace it through the payment system. ## S [#s] ### Sandbox [#sandbox] A test environment that simulates real payment processing without moving actual funds. Used for development and testing. ### SAQ (Self-Assessment Questionnaire) [#saq-self-assessment-questionnaire] A PCI DSS compliance validation tool. Different SAQ types (A, A-EP, D) apply based on how card data is handled. ### Settlement [#settlement] The process of transferring funds from the customer's bank to the merchant's bank after authorization. ### Skip Capture [#skip-capture] A flag that creates an authorization without automatic capture, enabling two-step payment flows. ### Subscription [#subscription] A recurring billing arrangement where customers are charged automatically on a schedule. ### Success Callback [#success-callback] A URL that Precium calls via webhook when a payment succeeds. ### Success Redirect [#success-redirect] The URL where customers are redirected after successful payment. ## T [#t] ### Token / Tokenization [#token--tokenization] The process of replacing sensitive payment data with a non-sensitive reference (token) that can be used for future transactions. ## U [#u] ### UUID (Universally Unique Identifier) [#uuid-universally-unique-identifier] A 36-character identifier in the format `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. Used for all Precium resource IDs. ## V [#v] ### Void [#void] See Cancel. ## W [#w] ### Webhook [#webhook] An HTTP callback that delivers event notifications to your application in real-time. Used for payment confirmations, failures, and other events. ### Whitelist [#whitelist] See Payment Method Whitelist. ## Z [#z] ### Zero-Amount Authorization [#zero-amount-authorization] An authorisation for R0.00 is used to verify a card and tokenise it without charging the customer. ## Payment Method Reference [#payment-method-reference] | Method | Type | Recurring | Refunds | |---|---|---|---| | Visa | Card | Yes | Yes | | Mastercard | Card | Yes | Yes | | Maestro | Card | Yes | Yes | | Ozow | Instant EFT | No | Via payout | | Capitec Pay | Instant EFT | No | Via payout | | Capitec Pay Recurring | Debit | Yes | Via payout | | DebiCheck | Debit | Yes | Via payout | | Debit Order | Debit | Yes | Via payout | | Zapper | Wallet | No | Via payout | ## Status Reference [#status-reference] ### Purchase Statuses [#purchase-statuses] | Status | Description | |---|---| | `created` | Purchase created, awaiting payment | | `pending_charge` | Authorisation in flight at the acquirer | | `paid` | Payment completed successfully | | `hold` | Pre-authorization successful, awaiting capture | | `cancelled` | Purchase was cancelled | | `released` | Pre-auth was released without capture | | `refunded` | Full refund processed | | `partially_refunded` | Partial refund processed | ### Subscription Statuses [#subscription-statuses] | Status | Description | |---|---| | `active` | Subscription is active and billing | | `paused` | Temporarily paused | | `cancelled` | Permanently cancelled | | `past_due` | Payment failed, awaiting retry | ## Currency Codes (ISO 4217) [#currency-codes-iso-4217] | Code | Currency | |---|---| | `ZAR` | South African Rand | | `USD` | US Dollar | | `EUR` | Euro | | `GBP` | British Pound | # Instant EFT (/docs/payment-orchestration/instant-eft) Instant EFT lets a customer pay from their bank account through a provider's hosted page, rather than with a card. Precium supports two providers. | Provider | `payment_method_whitelist` value | |---|---| | Ozow | `ozow` | | OneGate | `onegate_eft_redirect` | **The flow is identical.** OneGate is an alternative to Ozow, not a different integration. Choosing between them is a single string. ## The flow [#the-flow] Three steps, the same for both providers. ### 1. Create the client [step] [#1-create-the-client-step] ```bash POST https://gate.reviopay.com/api/v1/clients/ { "email": "customer@example.com", "full_name": "A Customer", "phone": "+27761234567" } ``` ### 2. Create the purchase [step] [#2-create-the-purchase-step] Set `payment_method_whitelist` to the provider you want. Restricting it to one method sends the customer straight to that provider instead of a method picker. ```bash POST https://gate.reviopay.com/api/v1/purchases/ { "client_id": "{client_id}", "brand_id": "{brand_id}", "purchase": { "currency": "ZAR", "language": "en", "products": [ { "name": "Order 1234", "price": 10000 } ] }, "payment_method_whitelist": ["onegate_eft_redirect"], "success_redirect": "https://www.example.com/success", "failure_redirect": "https://www.example.com/failure", "cancel_redirect": "https://www.example.com/cancel" } ``` Swap `onegate_eft_redirect` for `ozow` to route the same purchase to Ozow. Amounts are in **cents**: `10000` is ZAR 100.00. ### 3. Send the customer to the provider [step] [#3-send-the-customer-to-the-provider-step] Direct the customer's browser to the purchase's checkout URL. The provider hosts the bank selection and authorisation, then returns the customer to the redirect you supplied. Confirm the outcome from the webhook rather than from the redirect. A customer can close the browser after paying, and the redirect will not fire. ## The one difference [#the-one-difference] **OneGate does not require a mobile number.** If you send `phone` on the client, OneGate ignores it. Ozow uses it. Send it anyway if you have it. The same client record works for both providers, and you will not have to branch on which one a merchant is configured for. ## Which methods a brand actually has [#which-methods-a-brand-actually-has] Rather than hard-coding either identifier, ask: ```bash GET https://gate.reviopay.com/api/v1/payment_methods/?brand_id={brand_id}¤cy=ZAR ``` Send that with the same key you will create the purchase with, because the key decides whether the lookup runs against test or live configuration. A brand that is not enabled for a provider will not return it, and whitelisting a method a brand does not have will fail the purchase. # Integration guide (/docs/payment-orchestration/integration-guide) ## Introduction [#introduction] This comprehensive guide covers all payment scenarios, API endpoints, and integration patterns for the Precium Payment API. ## Authentication [#authentication] All API requests require Bearer token authentication. **Request Format** ```bash curl -X GET "https://gate.reviopay.com/api/v1/endpoint/" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" ``` **API Key Types** | Key Type | Usage | |---|---| | **Standard API Key** | All non-S2S operations | | **S2S API Key** | Server-to-server card operations (requires SAQ-D) | ## Client Management [#client-management] Clients represent your customers. Creating client records enables stored payment methods, pre-filled checkouts, and recurring billing. **Create Client** ```json POST https://gate.reviopay.com/api/v1/clients/ ``` **Request Body:** ```json { "email": "customer@example.com", "phone": "+27821234567", "full_name": "John Doe", "street_address": "123 Main Street", "city": "Cape Town", "state": "Western Cape", "zip_code": "8001", "country": "ZA", "shipping_street_address": "123 Main Street", "shipping_city": "Cape Town", "shipping_state": "Western Cape", "shipping_zip_code": "8001", "shipping_country": "ZA" } ``` **Parameters:** | Parameter | Type | Required | Description | |---|---|---|---| | `email` | string | **Yes** | Customer email address | | `phone` | string | No | Phone number (E.164 format) | | `full_name` | string | No | Customer full name | | `personal_code` | string | No | ID number (for DebiCheck) | | `street_address` | string | No | Billing address | | `city` | string | No | Billing city | | `state` | string | No | Billing state/province | | `zip_code` | string | No | Billing postal code | | `country` | string | No | Billing country (ISO 3166-1 alpha-2) | | `bank_account` | string | No | Bank account number | | `bank_code` | string | No | Bank branch/SWIFT code | | `cc` | array | No | CC email addresses for receipts | | `bcc` | array | No | BCC email addresses | **Response:** ```json { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "type": "client", "created_on": 1619740800, "updated_on": 1619740800, "email": "customer@example.com", "phone": "+27821234567", "full_name": "John Doe" } ``` **List Clients** ```json GET https://gate.reviopay.com/api/v1/clients/ ``` **Query Parameters:** | Parameter | Description | |---|---| | `q` | Fuzzy search across email, phone, name, etc. | | `email` | Search by email | | `phone` | Search by phone | **Get Client** ```bash GET https://gate.reviopay.com/api/v1/clients/{client_id}/ ``` **Update Client** ```bash PUT https://gate.reviopay.com/api/v1/clients/{client_id}/ ``` **Partial Update Client** ```bash PATCH https://gate.reviopay.com/api/v1/clients/{client_id}/ ``` **Delete Client** ```bash DELETE https://gate.reviopay.com/api/v1/clients/{client_id}/ ``` **List Client Recurring Tokens** Retrieve stored payment methods for a client: ```bash GET https://gate.reviopay.com/api/v1/clients/{client_id}/recurring_tokens/ ``` **Response:** ```json { "results": [ { "id": "tok_abc123...", "type": "recurring_token", "payment_method": "visa", "description": "Visa ending in 1111", "created_on": 1619740800 } ] } ``` **Delete Client Token** ```bash DELETE https://gate.reviopay.com/api/v1/clients/{client_id}/recurring_tokens/{token_id}/ ``` ## Purchase Management [#purchase-management] A Purchase represents a payment request. Create a purchase to generate a checkout URL or process a payment. **Create Purchase** ```bash POST https://gate.reviopay.com/api/v1/purchases/ ``` **Full Request Body:** ```json { "client_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "purchase": { "currency": "ZAR", "language": "en", "products": [ { "name": "Product Name", "price": 10000, "quantity": "1", "discount": 0, "tax_percent": "15", "category": "electronics" } ], "notes": "Special instructions", "due_strict": false }, "brand_id": "YOUR_BRAND_ID", "reference": "INV-2026-0001", "payment_method_whitelist": ["visa", "mastercard", "ozow"], "success_redirect": "https://www.example.com/success", "failure_redirect": "https://www.example.com/failure", "cancel_redirect": "https://www.example.com/cancel", "success_callback": "https://www.example.com/callback", "send_receipt": true, "force_recurring": false, "skip_capture": false, "due": 1735689600 } ``` **Core Parameters:** | Parameter | Type | Required | Description | |---|---|---|---| | `brand_id` | uuid | **Yes** | Your brand identifier | | `purchase.currency` | string | **Yes** | ISO 4217 currency code | | `purchase.products` | array | **Yes** | Array of product objects | | `purchase.products[].name` | string | **Yes** | Product name | | `purchase.products[].price` | integer | **Yes** | Price in cents | **Client Parameters (use `client_id` OR `client` object):** | Parameter | Type | Description | |---|---|---| | `client_id` | uuid | Existing client ID | | `client.email` | string | Create/find client by email | | `client.phone` | string | Client phone | | `client.full_name` | string | Client name | **Optional Parameters:** | Parameter | Type | Default | Description | |---|---|---|---| | `reference` | string | --- | Your order reference | | `payment_method_whitelist` | array | all | Allowed payment methods | | `success_redirect` | string | --- | Redirect URL on success | | `failure_redirect` | string | --- | Redirect URL on failure | | `cancel_redirect` | string | --- | Redirect URL on cancel | | `success_callback` | string | --- | Webhook URL for this purchase | | `send_receipt` | boolean | false | Email receipt on payment | | `force_recurring` | boolean | false | Force token storage | | `skip_capture` | boolean | false | Pre-authorization only | | `due` | integer | --- | Due date (Unix timestamp) | | `purchase.due_strict` | boolean | false | Block payment after due date | | `purchase.language` | string | en | Checkout language | **Response:** ```json { "id": "pur_abc123...", "type": "purchase", "status": "created", "created_on": 1619740800, "checkout_url": "https://gate.reviopay.com/p/abc123.../", "direct_post_url": "https://gate.reviopay.com/p/abc123.../post/", "purchase": { "currency": "ZAR", "total": 10000, "products": [...] }, "brand_id": "YOUR_BRAND_ID", "reference": "INV-2026-0001" } ``` **Get Purchase** ```bash GET https://gate.reviopay.com/api/v1/purchases/{purchase_id}/ ``` **List purchases for a client** There is no endpoint that lists every purchase. Purchases are listed per client, through the client's feed, which returns that client's purchases and refunds together. ```bash GET https://gate.reviopay.com/api/v1/clients/{client_id}/feed/ ``` `GET /api/v1/purchases/` does not exist. `/purchases/` accepts `POST` only. Retrieving a single purchase is `GET /api/v1/purchases/{purchase_id}/`, which is a different thing from listing them. Hold your own index of purchase ids against your orders. Reconciliation matches on `gateway_transaction_id`, which is that same id, so you need it stored on your side regardless. See [Reconciliation files](/docs/reconciliation). **Charge Purchase** Charge a purchase using a recurring token: ```bash POST https://gate.reviopay.com/api/v1/purchases/{purchase_id}/charge/ ``` **Request Body:** ```json { "recurring_token": "tok_abc123..." } ``` **Capture Pre-Authorization** Capture funds from a pre-authorized purchase: ```bash POST https://gate.reviopay.com/api/v1/purchases/{purchase_id}/capture/ ``` **Request Body:** ```json { "amount": 10000 } ``` **Release Pre-Authorization** Release held funds without capturing: ```bash POST https://gate.reviopay.com/api/v1/purchases/{purchase_id}/release/ ``` **Cancel Purchase** ```bash POST https://gate.reviopay.com/api/v1/purchases/{purchase_id}/cancel/ ``` **Resend Invoice** ```bash POST https://gate.reviopay.com/api/v1/purchases/{purchase_id}/resend_invoice/ ``` **Mark as Paid** Manually mark a purchase as paid (for offline payments): ```bash POST https://gate.reviopay.com/api/v1/purchases/{purchase_id}/mark_as_paid/ ``` ## Payment Scenarios [#payment-scenarios] ### Scenario 1: Payment Link (Minimal Integration) [#scenario-1-payment-link-minimal-integration] Generate a payment link to send via email or messaging: ```bash POST https://gate.reviopay.com/api/v1/purchases/ ``` ```json { "client": { "email": "customer@example.com" }, "purchase": { "currency": "ZAR", "products": [ { "name": "Invoice #12345", "price": 150000 } ] }, "brand_id": "YOUR_BRAND_ID" } ``` Share the `checkout_url` from the response with your customer. ### Scenario 2: E-commerce Checkout (Redirect Flow) [#scenario-2-e-commerce-checkout-redirect-flow] Standard e-commerce integration with redirect: ```mermaid sequenceDiagram participant C as Customer participant M as Your Site participant P as Precium C->>M: Click "Pay Now" M->>P: POST /purchases/ P-->>M: checkout_url M->>C: Redirect to checkout_url C->>P: Complete payment P->>C: Redirect to success_redirect P->>M: Webhook: purchase.paid M->>C: Show confirmation ``` **Create Purchase:** ```json { "client_id": "existing_client_id", "purchase": { "currency": "ZAR", "products": [ {"name": "Widget", "price": 5000, "quantity": "2"}, {"name": "Shipping", "price": 5000} ] }, "brand_id": "YOUR_BRAND_ID", "reference": "ORD-12345", "payment_method_whitelist": ["visa", "mastercard", "ozow"], "success_redirect": "https://www.example.com/order/ORD-12345/success", "failure_redirect": "https://www.example.com/order/ORD-12345/failure", "cancel_redirect": "https://www.example.com/cart" } ``` ### Scenario 3: Pre-Authorization (Hold Funds) [#scenario-3-pre-authorization-hold-funds] Reserve funds for later capture (hotels, car rentals): **Step 1: Create Pre-Auth Purchase** ```json { "client_id": "client_uuid", "purchase": { "currency": "ZAR", "products": [ {"name": "Hotel Deposit", "price": 500000} ] }, "brand_id": "YOUR_BRAND_ID", "skip_capture": true, "success_redirect": "https://www.example.com/booking/success", "failure_redirect": "https://www.example.com/booking/failure" } ``` **Step 2: Capture When Ready** ```bash POST https://gate.reviopay.com/api/v1/purchases/{purchase_id}/capture/ ``` ```json { "amount": 450000 } ``` **Or Release if Not Needed:** ```bash POST https://gate.reviopay.com/api/v1/purchases/{purchase_id}/release/ ``` ### Scenario 4: Restrict Payment Methods [#scenario-4-restrict-payment-methods] Only allow specific payment methods: ```json { "payment_method_whitelist": ["visa", "mastercard"], ... } ``` **Available Payment Methods:** | Value | Description | |---|---| | `visa` | Visa cards | | `mastercard` | Mastercard | | `maestro` | Maestro cards | | `ozow` | Ozow Instant EFT | | `capitec_pay` | Capitec Pay | | `capitec_pay_recurring` | Capitec Pay Recurring | | `zapper` | Zapper | | `debicheck` | DebiCheck | | `eft` | Traditional debit order / EFT collection | | `debicheck` | Authenticated debit order (DebiCheck) | ### Scenario 5: Invoice with Due Date [#scenario-5-invoice-with-due-date] Create an invoice that's due by a specific date: ```json { "client_id": "client_uuid", "purchase": { "currency": "ZAR", "due_strict": true, "products": [ {"name": "Consulting Services", "price": 2500000} ] }, "brand_id": "YOUR_BRAND_ID", "due": 1735689600, "reference": "INV-2026-0042" } ``` Setting `due_strict: true` prevents payment after the due date. ## Hosted Checkout [#hosted-checkout] The hosted checkout page is a PCI-compliant payment form hosted by Precium. ### Checkout URL [#checkout-url] After creating a purchase, redirect customers to the `checkout_url`: ```text https://gate.reviopay.com/p/{purchase_id}/ ``` ### Customization [#customization] Checkout appearance is configured via your Brand settings in the dashboard: - Logo - Colors - Business name - Support contact ### Checkout Flow [#checkout-flow] ```mermaid flowchart TD A[Customer arrives at checkout_url] --> B{Multiple payment methods?} B -->|Yes| C[Select payment method] B -->|No| D[Show payment form] C --> D D --> E[Enter payment details] E --> F{3DS Required?} F -->|Yes| G[Complete 3DS] F -->|No| H[Process payment] G --> H H --> I{Success?} I -->|Yes| J[Redirect to success_redirect] I -->|No| K[Redirect to failure_redirect] ``` ## Direct Post Integration [#direct-post-integration] Direct Post allows you to create a custom checkout form while keeping card data out of your servers (SAQ A-EP compliance). ### How It Works [#how-it-works] 1. Create a purchase to get a `direct_post_url` 2. Build an HTML form that POSTs directly to the `direct_post_url` 3. Card data goes directly from the browser to Precium 4. Customer is redirected based on the result ### PCI Compliance Note [#pci-compliance-note] Important: Direct Post integration requires PCI DSS SAQ A-EP compliance. Contact your account manager before implementing. ### Form Structure [#form-structure] ```html
``` ### Field Requirements [#field-requirements] | Field | Format | Validation | |---|---|---| | `pm` | string | Payment method: `visa`, `mastercard`, etc. | | `cardholder_name` | string | Latin letters, space, apostrophe, dot, dash. Max 30 chars | | `card_number` | string | Digits only, no spaces. Max 19 chars | | `expires` | string | `MM/YY` format | | `cvc` | string | 3-4 digits | | `remember_card` | string | `on` to save card | ### Direct Post for Other Payment Methods [#direct-post-for-other-payment-methods] ```html
```
```html
```
```json s2s:true pm:ozow phone:27123456789 ```
## Recurring Payments [#recurring-payments] ### Tokenizing Payment Methods [#tokenizing-payment-methods] To save a payment method for future charges, set `force_recurring: true`: ```json { "client_id": "client_uuid", "purchase": { "currency": "ZAR", "products": [ {"name": "Initial Payment", "price": 10000} ] }, "brand_id": "YOUR_BRAND_ID", "force_recurring": true, "success_redirect": "https://www.example.com/success" } ``` After successful payment, the purchase ID becomes a recurring token. ### Zero-Amount Tokenization [#zero-amount-tokenization] To tokenize a card without charging: ```json { "client_id": "client_uuid", "purchase": { "currency": "ZAR", "products": [ {"name": "Card Authorization", "price": 0} ] }, "brand_id": "YOUR_BRAND_ID", "skip_capture": true, "force_recurring": true, "success_redirect": "https://www.example.com/success" } ``` ### Charging with Recurring Token [#charging-with-recurring-token] **Step 1: Create Purchase for Upcoming Charge** ```bash POST https://gate.reviopay.com/api/v1/purchases/ ``` ```json { "client_id": "client_uuid", "purchase": { "currency": "ZAR", "products": [ {"name": "Monthly Subscription", "price": 29900} ] }, "brand_id": "YOUR_BRAND_ID", "reference": "SUB-2026-01" } ``` **Step 2: Charge Using Token** ```bash POST https://gate.reviopay.com/api/v1/purchases/{new_purchase_id}/charge/ ``` ```json { "recurring_token": "{original_purchase_id}" } ``` ### Listing Client Tokens [#listing-client-tokens] ```bash GET https://gate.reviopay.com/api/v1/clients/{client_id}/recurring_tokens/ ``` ## Billing Templates (Subscriptions) [#billing-templates-subscriptions] **No longer offered.** Subscription billing via billing templates is not sold to new integrations. This section is retained for existing implementations only. For recurring charges, use tokenisation and merchant-initiated charges — see *Recurring Payments* above. Billing templates automate recurring billing with configurable schedules. ### Create Billing Template [#create-billing-template] ```bash POST https://gate.reviopay.com/api/v1/billing_templates/ ``` ```json { "title": "Premium Monthly Subscription", "purchase": { "currency": "ZAR", "products": [ { "name": "Premium Plan", "price": 29900 } ] }, "brand_id": "YOUR_BRAND_ID", "is_subscription": true, "subscription_active": true, "subscription_period": 1, "subscription_period_units": "months", "number_of_billing_cycles": 12, "subscription_due_period": 3, "subscription_due_period_units": "days", "subscription_trial_periods": 0, "force_recurring": true, "success_redirect": "https://www.example.com/subscribe/success", "failure_redirect": "https://www.example.com/subscribe/failure" } ``` **Subscription Parameters:** | Parameter | Type | Description | |---|---|---| | `is_subscription` | boolean | Enable subscription mode | | `subscription_active` | boolean | Activate automatic billing | | `subscription_period` | integer | Billing frequency number | | `subscription_period_units` | string | `days`, `weeks`, `months`, `years` | | `number_of_billing_cycles` | integer | Total billing cycles (0 = infinite) | | `subscription_due_period` | integer | Days before due date to create invoice | | `subscription_trial_periods` | integer | Free trial periods | | `subscription_charge_period_end` | boolean | Charge at end of period | ### Add Subscriber [#add-subscriber] ```bash POST https://gate.reviopay.com/api/v1/billing_templates/{template_id}/add_subscriber/ ``` ```json { "client_id": "client_uuid", "invoice_reference": "SUB-CUST-001", "subscription_confirm_each_order": false, "send_invoice_on_add_subscriber": false, "send_invoice_on_charge_failure": true, "send_receipt": true } ``` **Response includes:** - `billing_template_client.id` - Subscription ID - `purchase.checkout_url` - URL for initial tokenization - `purchase.direct_post_url` - Direct post URL ### Add Subscriber with Override [#add-subscriber-with-override] Customise pricing for specific subscribers: ```json { "client_id": "client_uuid", "invoice_reference": "SUB-VIP-001", "override": { "products": [ { "name": "VIP Plan (Discounted)", "price": 19900 } ] } } ``` ### List Billing Templates [#list-billing-templates] ```bash GET https://gate.reviopay.com/api/v1/billing_templates/ ``` ### Get Billing Template [#get-billing-template] ```bash GET https://gate.reviopay.com/api/v1/billing_templates/{template_id}/ ``` ### List Subscribers [#list-subscribers] ```bash GET https://gate.reviopay.com/api/v1/billing_templates/{template_id}/clients/ ``` ### Update Subscriber [#update-subscriber] A subscriber is updated with `PATCH`, not `PUT`. The update is partial: send only the fields you are changing. ```bash PATCH https://gate.reviopay.com/api/v1/billing_templates/{template_id}/clients/{subscription_id}/ ``` There is **no operation to remove a subscriber**. The API exposes only `GET` and `PATCH` on this path. To stop billing a subscriber, change the subscription through `PATCH` or cancel the billing template. ## Refunds [#refunds] ### Full Refund [#full-refund] ```bash POST https://gate.reviopay.com/api/v1/purchases/{purchase_id}/refund/ ``` ```json {} ``` ### Partial Refund [#partial-refund] ```bash POST https://gate.reviopay.com/api/v1/purchases/{purchase_id}/refund/ ``` ```json { "amount": 5000 } ``` ### Refund Response [#refund-response] ```json { "id": "ref_abc123...", "type": "refund", "status": "pending", "amount": 5000, "purchase_id": "pur_abc123..." } ``` ### Refund Limitations [#refund-limitations] - Refunds must not exceed the original payment amount - Partial refunds can be issued multiple times until fully refunded - Some payment methods may not support refunds (check payment method details) ## Webhooks [#webhooks] Webhooks notify your application of payment events in real-time. ### Available Events [#available-events] Subscribe only to the events you act on. Event names are exact — a name that does not appear below will silently never fire. | Event | Fires when | |---|---| | `purchase.created` | A purchase has been created | | `purchase.viewed` | The payer opened the checkout | | `purchase.paid` | Payment captured and complete | | `purchase.payment_failure` | Payment failed | | `purchase.cancelled` | Purchase cancelled | | `purchase.hold` | Funds reserved by an authorisation | | `purchase.preauthorized` | Zero-amount card verification succeeded | | `purchase.captured` | A hold was captured | | `purchase.capture_failure` | Capture failed | | `purchase.released` | A hold was released without capture | | `purchase.release_failure` | Release failed | | `purchase.pending_charge` | Authorisation in flight at the acquirer | | `purchase.pending_capture` | Capture accepted, still processing | | `purchase.pending_release` | Release accepted, still processing | | `purchase.pending_refund` | Refund accepted, still processing | | `payment.refunded` | A refund completed. Treat this as the completion signal | | `purchase.refund_failure` | Refund failed after acceptance | | `payment.charged_back` | A chargeback was raised | | `payment.chargeback_reversed` | A chargeback was reversed in your favour | | `purchase.settled` | The transaction settled | | `purchase.recurring_token_deleted` | A stored card token was deleted | Note that `purchase.pending`, `purchase.authorized`, `purchase.refunded`, `subscription.created` and `subscription.cancelled` are **not** valid event names. Use `purchase.pending_charge`, `purchase.preauthorized` and `payment.refunded` instead. ### Create Webhook [#create-webhook] ```bash POST https://gate.reviopay.com/api/v1/webhooks/ ``` ```json { "url": "https://www.example.com/webhooks/precium", "events": ["purchase.paid", "purchase.payment_failure", "payment.refunded"] } ``` ### Webhook Payload [#webhook-payload] ```json { "id": "evt_abc123...", "event_type": "purchase.paid", "created_on": 1619740800, "data": { "id": "pur_abc123...", "status": "paid", "purchase": { "total": 10000, "currency": "ZAR" }, "reference": "ORD-12345", "payment_method_details": { "card": { "brand": "visa", "last4": "1111" } } } } ``` ### Webhook Security [#webhook-security] Verify webhook authenticity using the signature header: ```python import base64 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding # Callbacks are signed with ASYMMETRIC RSA. There is no 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/ def verify_webhook(raw_body: bytes, signature_b64: str, public_key_pem: str) -> bool: key = serialization.load_pem_public_key(public_key_pem.encode()) try: key.verify( base64.b64decode(signature_b64), raw_body, # raw bytes; never a re-serialised dict padding.PKCS1v15(), hashes.SHA256(), ) return True except Exception: return False ``` ### Webhook Best Practices [#webhook-best-practices] 1. **Respond quickly** — Return 200 within 5 seconds 2. **Process asynchronously** — Queue for background processing 3. **Handle duplicates** — Use event ID for idempotency 4. **Verify signatures** — Validate webhook authenticity ## Payouts [#payouts] **Not currently available.** Payouts are not yet released. The endpoints are documented here for reference only — do not build against them, and speak to your account manager before planning a disbursement flow. Send funds to recipients using stored payment methods. ### Create Payout [#create-payout] ```bash POST https://gate.reviopay.com/api/v1/payouts/ ``` ```json { "client": { "email": "recipient@example.com", "phone": "+27821234567" }, "payment": { "amount": 50000, "currency": "ZAR", "description": "Commission Payment" }, "brand_id": "YOUR_BRAND_ID" } ``` ### Execute Payout [#execute-payout] ```bash POST {execution_url} ``` ```json { "cardholder_name": "John Doe", "card_number": "4444333322221111", "expiry_month": "12", "expiry_year": "28" } ``` ### Get Payout Status [#get-payout-status] ```bash GET https://gate.reviopay.com/api/v1/payouts/{payout_id}/ ``` ## API Response Codes [#api-response-codes] | Code | Description | |---|---| | `200` | Success | | `201` | Created | | `204` | No content (successful deletion) | | `400` | Bad request (validation error) | | `401` | Unauthorized (invalid API key) | | `404` | Not found | | `409` | Conflict (duplicate request) | | `429` | Rate limited | | `500` | Server error | ## Rate Limiting [#rate-limiting] API requests are rate-limited to ensure fair usage: - **Standard:** 10 transactions per second, per IP address - **Burst:** 200 requests per minute (short periods) Rate limit headers: ```text X-RateLimit-Limit: 100 X-RateLimit-Remaining: 95 X-RateLimit-Reset: 1619740860 ``` ## Pagination [#pagination] List endpoints use cursor-based pagination: ```json { "results": [...], "next": "https://gate.reviopay.com/api/v1/purchases/?cursor=abc123", "previous": null } ``` Follow the `next` URL to retrieve additional pages. # Integration quick start (/docs/payment-orchestration/integration-quick-start) ## Precium API Quick Start Guide [#precium-api-quick-start-guide] Get your first payment working in under 10 minutes. ## Prerequisites [#prerequisites] Before you begin, ensure you have: - Precium account with API access - API Key (Bearer Token) from your dashboard - Brand ID from your dashboard. Your brand might need to be approved by [support@precium.com](mailto:support@precium.com) before you can utilise it for live transactions ## Step 1: Test Your API Connection [step] [#step-1-test-your-api-connection-step] Verify your credentials work: ```bash curl -X GET "https://gate.reviopay.com/api/v1/clients/" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Expected Response:** ```json { "results": [], "next": null, "previous": null } ``` If you receive a `401 Unauthorized` error, check your API key. ## Step 2: Create Your First Payment [step] [#step-2-create-your-first-payment-step] ### Option A: Payment Link (Simplest) [step] [#option-a-payment-link-simplest-step] Create a payment with minimal parameters: ```bash curl -X POST "https://gate.reviopay.com/api/v1/purchases/" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client": { "email": "customer@example.com" }, "purchase": { "currency": "ZAR", "products": [ { "name": "Test Product", "price": 10000 } ] }, "brand_id": "YOUR_BRAND_ID" }' ``` **Response:** ```json { "id": "pur_abc123...", "checkout_url": "https://gate.reviopay.com/p/abc123.../", "status": "created", "purchase": { "currency": "ZAR", "total": 10000, "products": [...] } } ``` Open the `checkout_url` in your browser to see the hosted checkout page. ### Option B: Redirect Flow (E-commerce) [step] [#option-b-redirect-flow-e-commerce-step] For e-commerce with a redirect back to your site: ```bash curl -X POST "https://gate.reviopay.com/api/v1/purchases/" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client": { "email": "customer@example.com" }, "purchase": { "currency": "ZAR", "products": [ { "name": "Test Product", "price": 10000 } ] }, "brand_id": "YOUR_BRAND_ID", "success_redirect": "https://www.example.com/payment/success", "failure_redirect": "https://www.example.com/payment/failure", "cancel_redirect": "https://www.example.com/payment/cancel" }' ``` After payment: - **Success** — Customer redirected to `success_redirect` - **Failure** — Customer redirected to `failure_redirect` - **Cancel** — Customer redirected to `cancel_redirect` ## Step 3: Complete a Test Payment [step] [#step-3-complete-a-test-payment-step] 1. Open the `checkout_url` from the response 2. Enter test card details: | Field | Value | |---|---| | Card Number | `4444 3333 2222 1111` | | Expiry | `12/28` (any future date) | | CVC | `123` | | Name | `Test User` | 1. Complete 3D Secure if prompted 2. Observe the redirect to your success URL 3. Remember to make use of your testing API credentials ## Step 4: Verify Payment Status [step] [#step-4-verify-payment-status-step] Check the purchase status: ```bash curl -X GET "https://gate.reviopay.com/api/v1/purchases/PURCHASE_ID/" \ -H "Authorization: Bearer YOUR_API_KEY" ``` **Successful Payment Response:** ```json { "id": "pur_abc123...", "status": "paid", "purchase": { "total": 10000, "currency": "ZAR" }, "payment_method_details": { "card": { "brand": "visa", "last4": "1111" } } } ``` ## Step 5: Set Up Webhooks (Recommended) [step] [#step-5-set-up-webhooks-recommended-step] Don't rely solely on redirects. Set up webhooks for reliable payment notifications. ### Create Webhook Subscription [step] [#create-webhook-subscription-step] Via Dashboard: 1. Navigate to Settings — Webhooks 2. Add webhook URL 3. Select events: `purchase.paid`, `purchase.payment_failure` Via API: ```bash curl -X POST "https://gate.reviopay.com/api/v1/webhooks/" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.example.com/webhooks/precium", "events": ["purchase.paid", "purchase.payment_failure"] }' ``` ### Handle Webhook [step] [#handle-webhook-step] ```jsx // Express.js example app.post('/webhooks/precium', (req, res) => { const event = req.body; switch (event.event_type) { case 'purchase.paid': // Fulfill order console.log('Payment received:', event.data.id); break; case 'purchase.payment_failure': // Handle failure console.log('Payment failed:', event.data.id); break; } res.status(200).send('OK'); }); ``` ## Quick Reference [#quick-reference] ### Purchase Statuses [#purchase-statuses] The ones you will meet most often. Note there is no bare `pending` status — the in-flight states are named for the operation. | Status | Meaning | |---|---| | `created` | Created, awaiting payment | | `sent` / `viewed` | Invoice sent / checkout opened | | `pending_charge` | Authorisation in flight | | `hold` | Funds reserved, awaiting capture or release | | `preauthorized` | Card verified, no money moved | | `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 due date | | `error` | Failed — read `transaction_data.attempts[]`, newest first | ### Essential Endpoints [#essential-endpoints] | Action | Method | Endpoint | |---|---|---| | Create purchase | POST | `/purchases/` | | Get purchase | GET | `/purchases/{id}/` | | Refund purchase | POST | `/purchases/{id}/refund/` | | Create client | POST | `/clients/` | | Get client | GET | `/clients/{id}/` | ### Test Cards [#test-cards] | Card Number | Behavior | |---|---| | `4444 3333 2222 1111` | Success | | `5555 5555 5555 4444` | Success (Mastercard) | Use CVC `123` and any future expiry date. ## Common Issues [#common-issues] ### "Invalid brand_id" [#invalid-brand_id] - Verify your brand_id in the dashboard - Ensure you're using the correct environment credentials ### "Unauthorized" [#unauthorized] - Check that your API key is correct - Ensure the Authorization header format: `Bearer YOUR_API_KEY` ### Checkout not loading [#checkout-not-loading] - Verify `success_redirect` and `failure_redirect` are valid URLs - Ensure the purchase hasn't expired ## Next Steps [#next-steps] Now that you've processed your first payment: 1. **Integration Guide** — Learn about all payment scenarios 2. **Code Examples** — Production-ready code samples 3. **Testing Guide** — Complete test scenarios ## Complete Example: E-commerce Checkout [#complete-example-e-commerce-checkout] ```python import requests API_KEY = "your_api_key" BRAND_ID = "your_brand_id" BASE_URL = "https://gate.reviopay.com/api/v1" def create_checkout(customer_email, amount_cents, product_name, order_id): """Create a checkout and return the redirect URL""" response = requests.post( f"{BASE_URL}/purchases/", headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json" }, json={ "client": { "email": customer_email }, "purchase": { "currency": "ZAR", "products": [{ "name": product_name, "price": amount_cents }] }, "brand_id": BRAND_ID, "reference": order_id, "success_redirect": f"https://www.example.com/orders/{order_id}/success", "failure_redirect": f"https://www.example.com/orders/{order_id}/failure", "cancel_redirect": f"https://www.example.com/orders/{order_id}/cancel" } ) data = response.json() return { "purchase_id": data["id"], "checkout_url": data["checkout_url"] } # Usage checkout = create_checkout( customer_email='customer@example.com', amount_cents=29900, # R299.00 product_name='Premium Subscription', order_id='ORD-12345' ) print(f"Redirect customer to: {checkout['checkout_url']}") ``` # Introduction (/docs/payment-orchestration/introduction) Welcome to the Precium Payment Orchestration API documentation. This comprehensive guide provides everything you need to integrate Precium's payment orchestration platform into your application, enabling you to accept payments across multiple payment methods through a single, unified API. ## What is Precium? [#what-is-precium] Precium is a payment orchestration platform that simplifies payment acceptance by providing: - **Unified API** — Single integration for multiple payment methods - **Hosted Checkout** — Secure, PCI-compliant payment pages - **Payment Links** — Quick payment collection via shareable links - **Recurring Billing** — Automated subscription management - **Multi-Method Support** — Cards, EFT, Instant EFT, DebiCheck, and more ## Who Is This Documentation For? [#who-is-this-documentation-for] This documentation is designed for: - **E-commerce Developers** building online stores and checkout experiences - **SaaS Platforms** implementing subscription billing - **Mobile App Developers** adding payment capabilities - **Business Systems Integrators** connecting billing and accounting systems - **Marketplace Operators** managing multi-party payments ## Integration Options [#integration-options] Precium offers flexible integration approaches to match your technical requirements and PCI compliance level: | Integration Type | PCI Scope | Description | Best For | |---|---|---|---| | **Hosted Checkout** | SAQ-A | Redirect to Precium-hosted payment page | Fastest integration, minimal compliance burden | | **Payment Links** | SAQ-A | Generate shareable payment URLs | Invoicing, email payments, manual billing | | **Direct Post** | SAQ A-EP | Custom checkout with form POST to Precium | Branded checkout experience | | **Server-to-Server** | SAQ-D | Full API control with card data handling | PSPs and enterprise with full PCI compliance | Note: This documentation covers Hosted Checkout, Payment Links, and Direct Post integrations. For Server-to-Server (S2S) integration requiring PCI SAQ-D compliance, refer to the S2S Integration Guide by reaching out to your sales/account manager. ## Supported Payment Methods [#supported-payment-methods] ### Cards [#cards] - Visa - Mastercard - Maestro - American Express (where available) ### Instant EFT [#instant-eft] - Ozow - OneGate - Capitec Pay ### Bank Debit [#bank-debit] - DebiCheck (authenticated debit orders) - Debit Order (traditional EFT debit) ### Bank Credit [#bank-credit] - SSVS - Same day value service allowing push payments to bank accounts - BEFT - Dated electronic bank transfer allowing next day push payments to bank accounts ### Mobile Wallets [#mobile-wallets] - Zapper - SnapScan ### Other [#other] - Capitec Pay Recurring - Manual EFT with payment reference ## API Overview [#api-overview] ### Base URL [#base-url] `https://gate.reviopay.com/api/v1/` ### Authentication [#authentication] All API requests require Bearer token authentication: ```bash curl -X GET "https://gate.reviopay.com/api/v1/purchases/" \ -H "Authorization: Bearer YOUR_API_KEY" ``` ### Core Resources [#core-resources] | Resource | Description | |---|---| | `/clients/` | Customer records with contact and billing information | | `/purchases/` | Payment transactions and invoices | | `/billing_templates/` | Subscription billing configurations — **no longer offered to new integrations** | | `/webhooks/` | Event notification subscriptions | | `/payouts/` | Disbursements to recipients — **not yet available** | ## Key Concepts [#key-concepts] ### Clients [#clients] A **Client** represents a customer in your system. Creating a client record enables: - Pre-filled checkout information - Stored payment methods for one-click payments - Recurring billing associations - Transaction history tracking ### Purchases [#purchases] A **Purchase** represents a payment request. Each purchase includes: - Products/line items with pricing - Currency and total amount - Payment method restrictions (optional) - Redirect URLs for payment completion - Checkout URL for hosted payment page ### Billing Templates [#billing-templates] A **Billing Template** defines recurring billing rules: - Subscription periods and pricing - Payment method requirements - Automated charge scheduling - Trial period configuration ### Recurring Tokens [#recurring-tokens] When a customer pays with `force_recurring: true`, their payment method is tokenized for future charges without requiring re-entry of payment details. ## Integration Architecture [#integration-architecture] ```mermaid flowchart LR subgraph Your["Your Application"] A[Backend Server] B[Frontend/Website] end subgraph Precium["Precium Platform"] C[API Gateway] D[Hosted Checkout] E[Payment Processing] end subgraph Methods["Payment Methods"] F[Card Networks] G[Bank Partners] H[Wallet Providers] end A -->|API Calls| C B -->|Redirect| D C --> E E --> F E --> G E --> H D -->|Callback| A D -->|Redirect| B ``` ## Documentation Structure [#documentation-structure] This documentation suite consists of the following guides: | Document | Description | |---|---| | **Quick Start** | Get your first payment working in minutes | | **Integration Guide** | Complete reference for all payment scenarios | | **Error Reference** | Error codes, causes, and resolution steps | | **Code Examples** | Production-ready code in multiple languages | | **Reconciliation** | Settlement and financial reporting | | **Testing Guide** | Test scenarios and sandbox testing | | **Troubleshooting** | Common issues and solutions | | **Glossary** | Payment terminology definitions | ## Getting Started [#getting-started] ### Prerequisites [#prerequisites] Before integrating, ensure you have: 1. **Precium Account** — Sign up at [https://gate.reviopay.com/login](https://gate.reviopay.com/login) 2. **API Credentials** — Obtain from your Precium Dashboard 3. **Brand Configuration** — Set up your brand. Multiple brands can be created 4. **Webhook Endpoint** — (Recommended) URL to receive payment notifications ### Quick Integration Path [#quick-integration-path] ```mermaid flowchart TD A[1. Create Client] --> B[2. Create Purchase] B --> C[3. Redirect to checkout_url] C --> D{Payment Completed?} D -->|Success| E[4. Receive Webhook] D -->|Failure| F[Handle Failure] E --> G[5. Fulfill Order] ``` ### Your First Payment [#your-first-payment] ```bash # 1. Create a purchase curl -X POST "https://gate.reviopay.com/api/v1/purchases/" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client": { "email": "customer@example.com" }, "purchase": { "currency": "ZAR", "products": [ { "name": "Product Name", "price": 10000 } ] }, "brand_id": "YOUR_BRAND_ID", "success_redirect": "https://www.example.com/success", "failure_redirect": "https://www.example.com/failure" }' # Response includes checkout_url - redirect customer there ``` ## Environment Information [#environment-information] | Environment | Base URL | Purpose | |---|---|---| | **Sandbox** | `https://gate.reviopay.com/api/v1/` | Testing and development | | **Production** | `https://gate.reviopay.com/api/v1/` | Live transactions | Note: Sandbox and Production use the same base URL but different API credentials. Ensure you use sandbox credentials for testing. ## Test Card Details [#test-card-details] For sandbox testing: | Field | Value | |---|---| | Card Number | `4444 3333 2222 1111` | | Expiry | Any future date | | CVC | `123` | | Cardholder | Any name (Latin characters) | ## Support & Resources [#support--resources] | Resource | Description | |---|---| | **API Reference** | [gate.reviopay.com/api/](https://gate.reviopay.com/api/) | | **Dashboard** | [gate.reviopay.com/login](https://gate.reviopay.com/login) | | **Support Email** | [support@precium.com](mailto:support@precium.com) | ## Next Steps [#next-steps] Ready to integrate? Start with the **Quick Start Guide** to process your first test payment in minutes. *© 2026 Precium. All rights reserved.* # Reconciliation (/docs/payment-orchestration/reconciliation) Note: The reconciliation service is currently available for card payments only. Support for additional payment methods will be added in future releases. ## Overview [#overview] 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 card transactions through the complete payment lifecycle, from authorisation to bank settlement. ## Report Types [#report-types] The reconciliation system produces three distinct report types: | 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 | ### Report Selection Guide [#report-selection-guide] ```mermaid flowchart TD A[Transaction Processed] --> B{Reconciled with Provider?} B -->|Yes| C[MARKOFF FILETransaction activity] B -->|No| D[Not in any report yet] C --> E{Settlement Data Available?} E -->|Yes| F[RECON FILEReady for settlement] E -->|No| G[EXCEPTION FILERequires investigation] ``` ## File Format [#file-format] ### Common Specifications [#common-specifications] | Attribute | Value | |---|---| | Encoding | UTF-8 | | Delimiter | Comma (`,`) | | Header Row | Always included | | Line Terminator | CRLF | | Timezone | UTC | ### Data Formats [#data-formats] | Type | Format | Example | |---|---|---| | Date | `YYYY-MM-DD` | `2026-01-15` | | Datetime | `YYYY-MM-DD HH:MM` | `2026-01-15 14:30` | | Decimal | `#.####` | `1234.5678` | | UUID | 36 characters | `a1b2c3d4-e5f6-...` | | Boolean | `True` / `False` | `True` | | Currency | ISO 4217 | `ZAR` | | Country | ISO 3166-1 alpha-2 | `ZA` | ## 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). ## Accessing Reports [#accessing-reports] ### SFTP Delivery [#sftp-delivery] Reconciliation reports are delivered automatically via SFTP to your designated location: - **Delivery Method:** Secure SFTP transfer - **Location:** Agreed upon during merchant onboarding - **Schedule:** Daily, typically available by 06:00 UTC - **Format:** CSV files as specified in this document Contact your account manager to configure or update your SFTP delivery settings. ### File Naming Convention [#file-naming-convention] Reports follow this naming pattern: ```text {report_type}_{merchant_id}_{date}.csv ``` Example: `recon_550e8400-e29b-41d4-a716-446655440000_2026-01-15.csv` ### Report Availability [#report-availability] | Report Type | Delivery | Contents | |---|---|---| | Recon File | T+2 morning | Settled transactions ready for reconciliation | | Markoff File | T+1 morning | All reconciled transactions (settlement not required) | | Exception File | T+1 morning | Unreconciled transactions requiring review | ## Reconciliation Workflow [#reconciliation-workflow] ```mermaid flowchart LR subgraph T0["T+0 Morning"] direction TB A1[Transactions processed] A2[Data captured] end subgraph Night["T+0 → T+1 Overnight"] direction TB B1[Gateway-provider recon] B2[Settlement processing] B3[Bank statement matching] end subgraph T1["T+1 Morning"] direction TB C1["ð Recon File"] C2["ð Markoff File"] C3["ð Exception File"] end T0 --> Night --> T1 ``` ## Settlement Timing [#settlement-timing] Settlement timing varies based on transaction characteristics and acquirer processing: ### Standard Settlement Timeline [#standard-settlement-timeline] | Transaction Type | Typical Settlement | Notes | |---|---|---| | 3D Secure authenticated | T+2 | Standard processing | | High-risk transactions | T+3 or longer | Extended fraud department clearance | ### Important Considerations [#important-considerations] Acquirer Fraud Review: Non-3DS transactions may experience delayed settlement (T+2 or T+3) as acquirers route these transactions through their fraud departments for additional clearance before releasing funds. This is standard industry practice for transactions without strong customer authentication. ### Factors Affecting Settlement Time [#factors-affecting-settlement-time] - **3DS Status:** Authenticated transactions typically settle faster - **Transaction Amount:** Higher-value transactions may undergo additional review - **Merchant Risk Profile:** Settlement timing may vary by merchant category - **Acquirer Policies:** Each acquirer has specific fraud review procedures - **Public Holidays:** Settlement may be delayed around bank holidays ### Projected vs Actual Clearance [#projected-vs-actual-clearance] The `projected_clearance_date` field in the Recon File provides an estimate, while `clearance_date` reflects actual bank clearance. Discrepancies may occur due to: - Acquirer fraud department holds - Bank processing delays - Weekend/holiday adjustments ## Settlement Amount Calculation [#settlement-amount-calculation] Understanding how net settlement is calculated: ```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 | R1,000.00 | | Precium Fee | -R25.00 | | Provider Fee | -R15.00 | | Acquirer Fee | -R10.00 | | **Net Settlement** | **R950.00** | ## Handling refunds [#handling-refunds] A refund is a separate payment transaction. It gets its own `gateway_transaction_id`, its own RRN and its own authorisation code, and it appears as its own row with `transaction_type` of `REFUND` and a **negative** `transaction_amount`. Recon v2.1 carried a `related_transaction_reference` column linking a refund back to the original purchase. **That column does not exist in v3.0.** If you need the linkage, record it on your side when you make the refund call. See [Reconciliation files](/docs/reconciliation). ## Exception Handling [#exception-handling] Transactions in the Exception File require investigation: **Common Exception Causes:** - Settlement data not received from the provider - Bank statement matching failure - Data discrepancy between systems **Resolution Process:** 1. Check the Exception File daily 2. Investigate unmatched transactions 3. Transactions auto-resolve when settlement data arrives 4. Contact support for persistent exceptions (>3 business days) ## Integration Best Practices [#integration-best-practices] 1. **Automate Downloads:** Schedule daily report downloads 2. **Match references:** use `gateway_transaction_reference` to link to your orders 3. **Monitor Exceptions:** Set up alerts for exception file entries 4. **Reconcile Daily:** Process files daily for timely issue detection 5. **Archive Reports:** Retain reports for audit purposes ## Coming Soon [#coming-soon] The following payment methods will be added to reconciliation: - Ozow - Capitec Pay - DebiCheck - Debit Orders Contact your account manager for timeline updates. ## Support [#support] For reconciliation questions or discrepancies: **Email:** support@precium.com **Subject:**`[Recon] {Merchant Name} - {Issue Description}` **Include:** - Merchant ID - Affected date(s) - Purchase ID(s) - Description of discrepancy # Technical reference documentation (/docs/payment-orchestration/technical-reference-documentation) ## Swagger reference [#swagger-reference] [https://gate.reviopay.com/api/](https://gate.reviopay.com/api/) ## Postman recipes [#postman-recipes] [Redirect and Direct Post](https://www.postman.com/reviopay/revio-pay/collection/dhmykuv/revio-payment-orchestration?action=share&source=copy-link&creator=25967279) *© 2026 Precium. All rights reserved.* # Test scenarios (/docs/payment-orchestration/test-scenarios) Comprehensive test scenarios and sandbox testing instructions for the Precium Payment API. Contact your account manager to gain access to more advanced testing scenarios if required. Especially for s2s integrations. ## Sandbox Environment [#sandbox-environment] ### Sandbox Configuration [#sandbox-configuration] | Setting | Value | |---|---| | Base URL | `https://gate.reviopay.com/api/v1/` | | API Key | Your sandbox API key from dashboard | | Brand ID | Your sandbox brand ID | Note: Sandbox and production use the same base URL but different credentials. Always verify you're using sandbox credentials when testing. ## Test Card Numbers [#test-card-numbers] ### Successful Payments [#successful-payments] | Card Number | Brand | 3DS | Behavior | |---|---|---|---| | `4444 3333 2222 1111` | Visa | Yes | Success | | `5555 5555 5555 4444` | Mastercard | Yes | Success | | `4000 0000 0000 0077` | Visa | No | Success (no 3DS) | ### Test Card Details [#test-card-details] | Field | Value | |---|---| | Expiry Date | Any future date (e.g., `12/28`) | | CVC | `123` | | Cardholder Name | Any Latin characters | ### Failed Payments [#failed-payments] Use these card numbers to simulate failures: | Card Number | Error Type | |---|---| | Invalid card number | Card declined | | Invalid CVC | Validation error | | Past expiry date | Expired card | ## Test Scenarios [#test-scenarios] ### Category 1: Basic Payment Flow [#category-1-basic-payment-flow] **TC-1.1: Create Payment Link** **Objective:** Verify payment link creation with minimal parameters **Steps:** 1. POST to `/purchases/` with client email, product, and brand_id 2. Verify response contains `checkout_url` 3. Open `checkout_url` in browser 4. Verify the checkout page loads correctly **Request:** ```json { "client": {"email": "test@example.com"}, "purchase": { "currency": "ZAR", "products": [{"name": "Test Product", "price": 10000}] }, "brand_id": "YOUR_BRAND_ID" } ``` **Expected:** HTTP 201, response contains `checkout_url` and `id` **TC-1.2: Complete Card Payment** **Objective:** End-to-end card payment flow **Steps:** 1. Create a purchase with redirect URLs 2. Navigate to `checkout_url` 3. Enter test card details 4. Complete 3DS if prompted 5. Verify the redirect to `success_redirect` 6. GET purchase to confirm the status is `paid` **Request:** ```json { "client": {"email": "test@example.com"}, "purchase": { "currency": "ZAR", "products": [{"name": "Test Product", "price": 10000}] }, "brand_id": "YOUR_BRAND_ID", "success_redirect": "https://example.com/success", "failure_redirect": "https://example.com/failure" } ``` **Expected:** - Payment completes - Redirected to the success URL - Purchase status is `paid` **TC-1.3: Payment with Multiple Products** **Objective:** Verify the correct total calculation **Steps:** 1. Create a purchase with multiple products 2. Verify `purchase.total` equals the sum of all product prices 3. Complete payment 4. Verify the amount charged matches the total **Request:** ```json { "client": {"email": "test@example.com"}, "purchase": { "currency": "ZAR", "products": [ {"name": "Product A", "price": 5000, "quantity": "2"}, {"name": "Product B", "price": 3000}, {"name": "Shipping", "price": 2000} ] }, "brand_id": "YOUR_BRAND_ID" } ``` **Expected:** Total should be 15000 (5000Ã2 + 3000 + 2000) ### Category 2: Payment Method Restrictions [#category-2-payment-method-restrictions] **TC-2.1: Whitelist Card Only** **Objective:** Verify that only whitelisted methods appear **Steps:** 1. Create a purchase with `payment_method_whitelist: ["visa", "mastercard"]` 2. Open checkout page 3. Verify that only Visa and Mastercard options appear 4. Verify EFT options are not available **Request:** ```json { "client": {"email": "test@example.com"}, "purchase": { "currency": "ZAR", "products": [{"name": "Test", "price": 10000}] }, "brand_id": "YOUR_BRAND_ID", "payment_method_whitelist": ["visa", "mastercard"] } ``` **Expected:** Only card payment options visible on checkout **TC-2.2: Whitelist EFT Only** **Objective:** Verify non-card methods work **Steps:** 1. Create a purchase with `payment_method_whitelist: ["ozow"]` 2. Open checkout page 3. Verify that only Ozow option appears 4. Verify card options are not available **Expected:** Only Ozow payment option visible ### Category 3: Pre-Authorization (Hold Funds) [#category-3-pre-authorization-hold-funds] **TC-3.1: Create Pre-Authorization** **Objective:** Reserve funds without capture **Steps:** 1. Create a purchase with `skip_capture: true` 2. Complete payment 3. Verify the status is `hold` (not `paid`) 4. Verify funds are reserved on the test card **Request:** ```json { "client_id": "CLIENT_UUID", "purchase": { "currency": "ZAR", "products": [{"name": "Hotel Deposit", "price": 500000}] }, "brand_id": "YOUR_BRAND_ID", "skip_capture": true, "success_redirect": "https://example.com/success", "failure_redirect": "https://example.com/failure" } ``` **Expected:** Status `hold` after payment completion **TC-3.2: Capture Pre-Authorization (Full)** **Objective:** Capture the full held amount **Steps:** 1. Create pre-auth purchase (TC-3.1) 2. Complete payment 3. POST to `/purchases/{id}/capture/` with full amount 4. Verify status changes to `paid` **Request:** ```bash POST https://gate.reviopay.com/api/v1/purchases/{id}/capture/ {"amount": 500000} ``` **Expected:** Status changes to `paid` **TC-3.3: Capture Pre-Authorization (Partial)** **Objective:** Capture less than held amount **Steps:** 1. Create pre-auth for 500000 cents 2. Complete payment 3. Capture only 300000 cents 4. Verify capture succeeds 5. Verify the remaining 200000 is released **Request:** ```bash POST https://gate.reviopay.com/api/v1/purchases/{id}/capture/ {"amount": 300000} ``` **Expected:** Capture succeeds with a partial amount **TC-3.4: Release Pre-Authorization** **Objective:** Release held funds without capture **Steps:** 1. Create a pre-auth purchase 2. Complete payment (status = `hold`) 3. POST to `/purchases/{id}/release/` 4. Verify status changes to `released` 5. Verify funds are released on the test card **Expected:** Status `released`, funds returned ### Category 4: Refunds [#category-4-refunds] **TC-4.1: Full Refund** **Objective:** Refund the entire payment **Steps:** 1. Create and complete a payment 2. Verify the status is `paid` 3. POST to `/purchases/{id}/refund/` with an empty body 4. Verify the refund is processed 5. Check the purchase for refund status **Request:** bash ```bash POST https://gate.reviopay.com/api/v1/purchases/{id}/refund/ {} ``` **Expected:** Refund processed, status updated **TC-4.2: Partial Refund** **Objective:** Refund portion of payment **Steps:** 1. Complete payment for 10000 cents 2. POST refund for 3000 cents 3. Verify partial refund succeeds 4. Verify the remaining balance is 7000 **Request:** ```bash POST https://gate.reviopay.com/api/v1/purchases/{id}/refund/ {"amount": 3000} ``` **Expected:** Partial refund of 3000 cents processed **TC-4.3: Multiple Partial Refunds** **Objective:** Issue multiple refunds on the same purchase **Steps:** 1. Complete payment for 10000 cents 2. Refund 3000 cents (success) 3. Refund another 4000 cents (success) 4. Verify the total refunded is 7000 5. Verify the remaining is 3000 **Expected:** Multiple refunds succeed up to the original amount **TC-4.4: Refund Exceeds Amount** **Objective:** Verify refund limit enforcement **Steps:** 1. Complete payment for 10000 cents 2. Attempt to refund 15000 cents 3. Verify request fails with appropriate error **Expected:** HTTP 400, error `refund_exceeds_amount` ### Category 5: Recurring Payments [#category-5-recurring-payments] **TC-5.1: Tokenize Card** **Objective:** Save card for future payments **Steps:** 1. Create a purchase with `force_recurring: true` 2. Complete payment 3. GET `/clients/{id}/recurring_tokens/` 4. Verify the token is created **Request:** ```json { "client_id": "CLIENT_UUID", "purchase": { "currency": "ZAR", "products": [{"name": "Subscription", "price": 29900}] }, "brand_id": "YOUR_BRAND_ID", "force_recurring": true, "success_redirect": "https://example.com/success", "failure_redirect": "https://example.com/failure" } ``` **Expected:** Recurring token created after payment **TC-5.2: Zero-Amount Tokenization** **Objective:** Save the card without charging **Steps:** 1. Create a purchase with `price: 0` and `skip_capture: true` 2. Complete checkout (no charge) 3. Verify that the token is created 4. Use the token for subsequent charges **Request:** ```json { "client_id": "CLIENT_UUID", "purchase": { "currency": "ZAR", "products": [{"name": "Card Authorization", "price": 0}] }, "brand_id": "YOUR_BRAND_ID", "skip_capture": true, "force_recurring": true, "success_redirect": "https://example.com/success", "failure_redirect": "https://example.com/failure" } ``` **Expected:** Token created, no charge to card **TC-5.3: Charge with Token** **Objective:** Process payment using saved token **Steps:** 1. Complete tokenization (TC-5.1) 2. Create a new purchase for the same client 3. POST to `/purchases/{id}/charge/` with `recurring_token` 4. Verify payment succeeds without customer interaction **Request:** ```bash POST https://gate.reviopay.com/api/v1/purchases/{new_id}/charge/ {"recurring_token": "ORIGINAL_PURCHASE_ID"} ``` **Expected:** Payment succeeds, status `paid` **TC-5.4: Charge with Invalid Token** **Objective:** Verify invalid token handling **Steps:** 1. Create purchase 2. Attempt charge with non-existent token ID 3. Verify the appropriate error returned **Expected:** HTTP 404, error `recurring_token_not_found` ### Category 6: Billing Templates (Subscriptions) [#category-6-billing-templates-subscriptions] **TC-6.1: Create Subscription Plan** **Objective:** Create a monthly subscription template **Steps:** 1. POST to `/billing_templates/` 2. Verify template created 3. Check subscription settings are correct **Request:** ```json { "title": "Monthly Premium", "purchase": { "currency": "ZAR", "products": [{"name": "Premium Plan", "price": 29900}] }, "brand_id": "YOUR_BRAND_ID", "is_subscription": true, "subscription_active": true, "subscription_period": 1, "subscription_period_units": "months", "force_recurring": true } ``` **Expected:** Template created with subscription configuration **TC-6.2: Add Subscriber** **Objective:** Subscribe the client to a plan **Steps:** 1. Attempt to create another with the same email **TC-6.3: Add Subscriber with Override** **Objective:** Subscribe with custom pricing **Steps:** 1. Create a template with a standard price 2. Add subscriber with override price 3. Verify the subscriber has a custom price 4. Complete payment 5. Verify the charged amount matches the override **Request:** ```json { "client_id": "CLIENT_UUID", "override": { "products": [{"name": "Discounted Plan", "price": 19900}] } } ``` **Expected:** Subscription uses override pricing ### Category 7: Client Management [#category-7-client-management] **TC-7.1: Create Client** **Objective:** Create a new client record **Steps:** 1. POST to `/clients/` with email and details 2. Verify client created 3. Verify all fields are saved correctly **Request:** ```json { "email": "test@example.com", "full_name": "Test User", "phone": "+27821234567" } ``` **Expected:** HTTP 201, client ID returned **TC-7.2: Search Client** **Objective:** Find a client by email **Steps:** 1. Create client 2. GET `/clients/?email=test@example.com` 3. Verify the client found in the results **Expected:** Client returned in search results **TC-7.3: Duplicate Client Prevention** **Objective:** Verify duplicate email handling **Steps:** 1. Create a client with an email 2. Attempt to create another with the same email 3. Verify the appropriate error **Expected:** HTTP 409, error `duplicate_client` ### Category 8: Webhooks [#category-8-webhooks] **TC-8.1: Payment Success Webhook** **Objective:** Verify webhook delivery on payment **Steps:** 1. Configure the webhook for `purchase.paid` 2. Create and complete a payment 3. Verify the webhook was received 4. Verify payload contains purchase data **Expected:** Webhook delivered with correct event type **TC-8.2: Payment Failure Webhook** **Objective:** Verify the webhook on failed payment **Steps:** 1. Configure the webhook for `purchase.payment_failure` 2. Create purchase 3. Attempt payment with an invalid card 4. Verify failure webhook received **Expected:** Webhook delivered with failure details **TC-8.3: Webhook Signature Verification** **Objective:** Verify webhook authenticity **Steps:** 1. Receive webhook 2. Extract signature header 3. Calculate the expected signature using the secret 4. Verify signatures match **Expected:** Calculated signature matches header ### Category 9: Error Handling [#category-9-error-handling] **TC-9.1: Invalid API Key** **Objective:** Verify authentication error **Steps:** 1. Make a request with an invalid API key 2. Verify HTTP 401 returned 3. Verify the error message is clear **Expected:** HTTP 401, `invalid_token` error **TC-9.2: Missing Required Field** **Objective:** Verify validation error **Steps:** 1. POST purchase without `brand_id` 2. Verify HTTP 400 returned 3. Verify field-specific error message **Expected:** HTTP 400, `required` error for `brand_id` **TC-9.3: Invalid UUID** **Objective:** Verify UUID validation **Steps:** 1. GET `/purchases/invalid-uuid/` 2. Verify the appropriate error **Expected:** HTTP 400 or 404 with clear error **TC-9.4: Rate Limiting** **Objective:** Verify rate limit handling **Steps:** 1. Make requests rapidly (>100/minute) 2. Observe rate limit response 3. Verify the retry-after information provided **Expected:** HTTP 429 after limit exceeded ## Test Checklist [#test-checklist] **Pre-Integration Checklist** - Sandbox API key obtained - Brand ID configured - Webhook endpoint ready - Test card numbers available **Basic Flow Tests** - TC-1.1: Create payment link - TC-1.2: Complete card payment - TC-1.3: Multiple products - TC-2.1: Payment method whitelist **Advanced Flow Tests** - TC-3.1: Pre-authorization - TC-3.2: Full capture - TC-3.3: Partial capture - TC-3.4: Release hold - TC-4.1: Full refund - TC-4.2: Partial refund - TC-5.1: Card tokenization - TC-5.3: Token charge - TC-6.1: Create subscription - TC-6.2: Add subscriber **Error Handling Tests** - TC-9.1: Invalid API key - TC-9.2: Validation errors - TC-9.3: Invalid UUID - TC-9.4: Rate limiting **Production Readiness** - Webhook handling implemented - Error handling complete - Idempotency implemented - Logging configured - Production credentials ready ## Go-Live Checklist [#go-live-checklist] Before going live: 1. **Credentials** - Production API key configured - Production brand ID configured - Sandbox credentials removed from production 1. **Webhooks** - Production webhook URL configured - Webhook signature verification implemented - All relevant events subscribed 1. **Error Handling** - All error codes handled gracefully - User-friendly error messages displayed - Errors logged for debugging 1. **Security** - API keys stored securely (not in code) - HTTPS enforced - Webhook signatures verified 1. **Testing** - End-to-end test in production sandbox - Small live transaction tested - Refund tested # Troubleshooting (/docs/payment-orchestration/troubleshooting) ## Precium API Troubleshooting Guide [#precium-api-troubleshooting-guide] Solutions to common integration issues and debugging techniques. ## Quick Diagnostics [#quick-diagnostics] ### API Health Check [#api-health-check] ```bash curl -X GET "https://gate.reviopay.com/api/v1/clients/" \ -H "Authorization: Bearer YOUR_API_KEY" ``` | Response | Meaning | Action | |---|---|---| | `200` with results | API working | Integration issue | | `401 Unauthorized` | Invalid API key | Check credentials | | `500 Server Error` | Platform issue | Contact support | | Connection refused | Network issue | Check connectivity | ## Authentication Issues [#authentication-issues] ### Problem: 401 Unauthorized [#problem-401-unauthorized] **Symptoms:** ```json { "__all__": { "message": "Invalid or expired authentication token", "code": "invalid_token" } } ``` **Common Causes:** 1. **Incorrect API Key** ```bash # Wrong -H "Authorization: your_api_key" # Correct -H "Authorization: Bearer your_api_key" ``` 1. **Environment Mismatch** - Using the sandbox key in production - Using the production key in the sandbox 1. **Revoked Key** - The key was regenerated in the dashboard - The key was disabled **Resolution:** 1. Verify key format includes "Bearer " prefix 2. Check that the key in the dashboard matches the configuration 3. Generate a new key if necessary 4. Ensure the environment matches ### Problem: Missing Authorization Header [#problem-missing-authorization-header] **Symptoms:** ```json { "__all__": { "message": "Authorization header is required", "code": "missing_authorization" } } ``` **Resolution:** Ensure every request includes the header: ```python headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } ``` ## Purchase Creation Issues [#purchase-creation-issues] ### Problem: Invalid brand_id [#problem-invalid-brand_id] **Symptoms:** ```json { "brand_id": { "message": "Invalid pk \"xxx\" - object does not exist", "code": "does_not_exist" } } ``` **Causes:** - Incorrect brand ID - Brand ID from a different environment - Typo in UUID **Resolution:** 1. Copy brand_id directly from the dashboard 2. Verify format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` 3. Ensure the use of the correct environment's brand ### Problem: checkout_url Not Generated [#problem-checkout_url-not-generated] **Symptoms:** - `checkout_url` is `null` in response - `direct_post_url` is `null` **Causes:** - Missing `success_redirect` or `failure_redirect` - `purchase.request_client_details` is not empty - Purchase is not in a payable state **Resolution:** Include redirect URLs: ```json { "success_redirect": "https://www.example.com/success", "failure_redirect": "https://www.example.com/failure", ... } ``` ### Problem: Total Amount Mismatch [#problem-total-amount-mismatch] **Symptoms:** - Calculated total doesn't match expected - Checkout shows the wrong amount **Diagnosis:** ```python # Check how total is calculated total = sum( product['price'] * int(product.get('quantity', '1')) for product in products ) ``` **Common Issues:** - Quantity is a string, not applied correctly - Discount/tax calculations - Override fields not set properly **Resolution:** ```json { "products": [ {"name": "Item", "price": 5000, "quantity": "2"} // quantity is STRING ], "total_override": null // Don't set unless intentional } ``` ## Payment Processing Issues [#payment-processing-issues] ### Problem: Card Declined [#problem-card-declined] **Symptoms:** - Payment fails at checkout - Error message: "Card declined" **Possible Causes:** | Decline Reason | Meaning | Solution | |---|---|---| | `do_not_honour` | Generic decline | Try different card | | `insufficient_funds` | Not enough balance | Use different card | | `expired_card` | Card expired | Use valid card | | `invalid_card` | Card number invalid | Check digits | | `suspected_fraud` | Fraud detection | Contact bank | **Debugging:** ```bash # Check purchase for detailed error GET https://gate.reviopay.com/api/v1/purchases/{id}/ # Response includes: { "status": "failed", "payment_failure_reason": "do_not_honour" } ``` ### Problem: 3DS Authentication Failed [#problem-3ds-authentication-failed] **Symptoms:** - Customer fails 3D Secure challenge - Redirect to failure URL - Error: `3ds_authentication_failed` **Causes:** - The customer entered the wrong OTP/password - 3DS timeout - Customer cancelled - Browser pop-up blocked **Resolution:** 1. The customer should retry with the correct credentials 2. Ensure pop-ups are not blocked 3. Verify redirect URLs load quickly 4. Check browser compatibility ### Problem: Payment Stuck in Pending [#problem-payment-stuck-in-pending] **Symptoms:** - Status remains `pending_charge` indefinitely - No success/failure redirect **Diagnosis:** ```bash # Check current status GET https://gate.reviopay.com/api/v1/purchases/{id}/ # Response { "status": "pending", "events": [...], // Check for processing events "payment_method_details": {...} } ``` **Causes:** - 3DS flow not completed - Customer abandoned payment - Processing delay **Resolution:** 1. Wait for processing (usually \< 60 seconds) 2. Check if the customer completed 3DS 3. Set up webhooks for async notification 4. For extended pending, contact support ## Webhook Issues [#webhook-issues] ### Problem: Webhooks Not Received [#problem-webhooks-not-received] **Checklist:** - Webhook URL is HTTPS - URL is publicly accessible - Firewall allows Precium IPs - Server responds within 5 seconds - Events are subscribed to **Debug Steps:** 1. **Verify webhook configuration:** ```bash GET https://gate.reviopay.com/api/v1/webhooks/ # Check your endpoints are listed ``` 1. **Test URL accessibility:** ```bash curl -X POST "https://www.example.com/webhooks/precium" \ -H "Content-Type: application/json" \ -d '{"test": true}' ``` 1. **Check server logs for incoming requests** 2. **Verify the SSL certificate is valid** ### Problem: Webhook Signature Invalid [#problem-webhook-signature-invalid] **Symptoms:** - Signature verification fails - Events appear legitimate, but don't validate **Debug:** ```python import base64 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding # Signing is ASYMMETRIC RSA — there is no shared secret to compare against. # The two most common causes of a failure here are verifying a re-serialised # body instead of the raw bytes, and using the wrong public key. def debug_signature(raw_body: bytes, received_sig: str, public_key_pem: str) -> bool: print(f"X-Signature: {received_sig}") print(f"Raw body length: {len(raw_body)} bytes") key = serialization.load_pem_public_key(public_key_pem.encode()) try: key.verify( base64.b64decode(received_sig), raw_body, padding.PKCS1v15(), hashes.SHA256(), ) print("Valid") return True except Exception as exc: print(f"Invalid: {exc}") print("Check: raw bytes (not json.dumps output)? correct key for this webhook?") return False ``` **Common Issues:** 1. **Payload modification:** Use raw request body ```python # Wrong - parsed JSON payload = json.dumps(request.json) # Correct - raw body payload = request.get_data(as_text=True) ``` 1. **Encoding issues:** Ensure UTF-8 2. **Wrong secret:** Verify the secret from the dashboard ### Problem: Duplicate Webhook Events [#problem-duplicate-webhook-events] **Cause:** Webhook retry on non-200 response **Resolution:** ```python # Implement idempotency processed_events = set() def handle_webhook(event): event_id = event.get('id') if event_id in processed_events: return # Already processed processed_events.add(event_id) # Process event... ``` **Best Practice:** Store event IDs in the database: ```sql CREATE TABLE webhook_events ( event_id VARCHAR(50) PRIMARY KEY, event_type VARCHAR(50), processed_at TIMESTAMP DEFAULT NOW() ); ``` ## Recurring Payment Issues [#recurring-payment-issues] ### Problem: Token Not Created [#problem-token-not-created] **Symptoms:** - `force_recurring: true` set but no token - Empty token list for client **Causes:** - Payment not completed - The payment amount was 0 without `skip_capture` - Token creation is disabled for the payment method **Debug:** ```bash # Check if payment completed GET https://gate.reviopay.com/api/v1/purchases/{id}/ # Verify status is "paid" or "hold" # List client tokens GET https://gate.reviopay.com/api/v1/clients/{client_id}/recurring_tokens/ ``` **Resolution:** For zero-amount tokenization: ```json { "purchase": { "products": [{"name": "Auth", "price": 0}] }, "skip_capture": true, "force_recurring": true } ``` ### Problem: Token Charge Failed [#problem-token-charge-failed] **Symptoms:** - `/charge/` endpoint returns error - Token exists, but won't charge **Error Types:** | Error | Cause | Solution | |---|---|---| | `recurring_token_not_found` | Invalid token ID | Verify token exists | | `recurring_token_expired` | Card expired | Re-tokenize | | `card_declined` | Insufficient funds | Contact customer | **Debug:** ```bash # Verify token exists GET https://gate.reviopay.com/api/v1/clients/{client_id}/recurring_tokens/{token_id}/ # Check original purchase GET https://gate.reviopay.com/api/v1/purchases/{token_id}/ ``` ## Direct Post Issues [#direct-post-issues] ### Problem: direct_post_url is null [#problem-direct_post_url-is-null] **Causes:** - Missing redirect URLs - `purchase.request_client_details` not empty - Purchase not in created state **Resolution:** ```json { "success_redirect": "https://...", // Required "failure_redirect": "https://...", // Required "purchase": { "request_client_details": [] // Must be empty } } ``` ### Problem: Direct Post Returns to Failure [#problem-direct-post-returns-to-failure] **Symptoms:** - Valid card details - Redirect to failure URL **Debug Checklist:** - Form fields match expected names exactly - Card number has no spaces - Expiry format is `MM/YY` - CVC is 3-4 digits - Cardholder name uses Latin characters only **Field Requirements:** ```html ``` ## Performance Issues [#performance-issues] ### Problem: Slow API Responses [#problem-slow-api-responses] **Diagnosis:** ```python import time start = time.time() response = requests.get(url, headers=headers) elapsed = time.time() - start print(f"Response time: {elapsed:.2f}s") ``` **Causes:** - Network latency - Large response payloads - Server load **Resolution:** 1. Implement connection pooling 2. Use pagination for list endpoints 3. Cache responses where appropriate 4. Implement timeout handling ### Problem: Rate Limit Exceeded [#problem-rate-limit-exceeded] **Symptoms:** ```json HTTP 429 Too Many Requests Headers: X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1619740860 ``` **Resolution:** ```python import time def api_request_with_backoff(func, max_retries=3): for attempt in range(max_retries): response = func() if response.status_code == 429: reset_time = int(response.headers.get('X-RateLimit-Reset', 0)) wait = max(reset_time - time.time(), 2 ** attempt) print(f"Rate limited. Waiting {wait:.0f}s...") time.sleep(wait) continue return response raise Exception("Rate limit exceeded after retries") ``` ## Debugging Tools [#debugging-tools] ### Request Logging [#request-logging] ```python import logging logging.basicConfig(level=logging.DEBUG) # Log all requests import http.client http.client.HTTPConnection.debuglevel = 1 ``` ### Response Inspection [#response-inspection] ```python def debug_response(response): print(f"Status: {response.status_code}") print(f"Headers: {dict(response.headers)}") print(f"Body: {response.text}") ``` ### cURL Verbose Mode [#curl-verbose-mode] ```bash curl -v -X POST "https://gate.reviopay.com/api/v1/purchases/" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"..."}' ``` ## Error Message Reference [#error-message-reference] | Error | HTTP | Quick Fix | |---|---|---| | `invalid_token` | 401 | Check API key format | | `required` | 400 | Add missing field | | `does_not_exist` | 404 | Verify UUID | | `duplicate_client` | 409 | Search existing client | | `purchase_already_paid` | 409 | Check status first | | `recurring_token_expired` | 409 | Re-tokenize card | | `rate_limit_exceeded` | 429 | Implement backoff | ## Getting Support [#getting-support] ### Before Contacting Support [#before-contacting-support] 1. **Collect information:** - Request ID (if available) - Full error response - Request payload (sanitized) - Timestamp of issue 1. **Check dashboard:** - Transaction history - Webhook logs - Event logs 1. **Review documentation:** - API reference - Error codes guide ### Contact Support [#contact-support] **Email:** support@precium.com **Include:** - Merchant ID / Brand ID - Environment (Sandbox/Production) - Purchase ID(s) affected - Steps to reproduce - Error messages - Timestamps # Reconciliation files (/docs/reconciliation) Precium delivers reconciliation data as CSV over SFTP. This page describes the files as they are produced today, against **Recon File Specification v3**. The recon file format is versioned and has changed twice. Column names differ between versions, so a parser written against an older file will not find its columns in a newer one. Confirm which version your SFTP account receives before you build against this page. ## The three files [#the-three-files] | File | What it contains | Settlement required | |---|---|---| | **Recon** | Transactions reconciled against settlement data, ready for settlement in an upcoming cycle. The authoritative comparison dataset for a cycle. | Yes | | **Markoff** | Transactions reconciled between Precium's gateways and the providers, within the timeframe the file covers. | No | | **Exception** | Transactions that could not be reconciled within the expected timeframe. Populated best effort, because only partial data exists. | n/a | An exception row is not a failed payment. It means settlement for that transaction was still outstanding when the file for that date was cut, so it appears in the exception file instead of the recon file. It may reconcile normally in a later cycle. ## Parsing rules [#parsing-rules] These apply to every file unless stated otherwise. | Rule | Value | |---|---| | Encoding | UTF-8 | | Format | CSV, header row always present | | Delimiter | Comma. Values containing a comma are quoted with `"` | | Line terminator | CRLF | | Dates | `YYYY-MM-DD` | | Datetimes | `YYYY-MM-DD HH:MM` | | Timezone | UTC throughout | | Monetary values | Period decimal separator, rounded to **4 decimal places** | **Two things will break a naive parser.** **Leading apostrophes.** Numeric-looking values that must be read as strings are prefixed with a single quote (`'`) so that Excel does not reformat them. In the v3 file this affects `settlement_bank_account_number`, `gateway_transaction_reference`, `transaction_rrn` and `transaction_auth_code`. Strip the leading `'` before matching. **Amounts are in major units, not cents.** `transaction_amount` is `40.0000` for ZAR 40.00. This is the opposite of the API, where amounts are in cents. Refunds and chargebacks carry a **negative** amount. ## Recon file v3.0 [#recon-file-v30] 27 columns. One row per transaction. ### Identification [#identification] | Column | Type | Notes | |---|---|---| | `merchant_name` | string | Merchant display name | | `merchant_id` | uuid | Merchant identifier | | `brand_name` | string | Brand display name | | `brand_id` | uuid | Brand identifier | | `gateway_transaction_id` | uuid | **Precium gateway transaction identifier. This is your primary matching key.** | | `gateway_transaction_reference` | string | Your own reference, as supplied on the purchase. Prefixed with `'` | ### Transaction [#transaction] | Column | Type | Notes | |---|---|---| | `transaction_type` | enum | `PURCHASE`, `REFUND`, `CHARGEBACK` | | `transaction_amount` | decimal | Major units, 4 dp. Negative for refunds and chargebacks. Defaults to `0.0000` | | `transaction_currency` | string | ISO 4217 | | `transaction_date_time_utc` | datetime | `YYYY-MM-DD HH:MM`, UTC | | `transaction_payment_method` | enum | For example `CARD` | | `transaction_rrn` | string | Retrieval Reference Number. Prefixed with `'` | | `transaction_auth_code` | string | Authorisation code. Prefixed with `'` | ### Card [#card] Every field below is populated **only** when `transaction_payment_method` is `CARD`, and is empty otherwise. | Column | Type | Notes | |---|---|---| | `card_country` | string | Issuing country, ISO 3166-1 alpha-2 | | `card_type` | enum | `DEBIT`, `CREDIT` | | `card_authenticated` | boolean | `TRUE` / `FALSE`. Whether 3D Secure authentication was applied | | `card_tokenized` | boolean | `TRUE` / `FALSE`. Whether tokenisation was applied | | `card_issuer` | string | Card scheme or issuer, for example `VISA`, `MASTERCARD` | ### Settlement and fees [#settlement-and-fees] | Column | Type | Notes | |---|---|---| | `settlement_acquiring_bank` | enum | `ABSA`, `STANDARD_BANK`, `FNB`, `UNKNOWN` | | `settlement_bank_account_number` | string | Destination account. Prefixed with `'` | | `settlement_date` | date | Reporting settlement date | | `fee_codes` | string | Comma-separated list of fee codes applied. Quoted when it contains commas. Empty when no fees apply | | `invoiced_fee_total_ex_vat` | decimal | Defaults to `0.0000` | | `invoiced_fee_total_incl_vat` | decimal | Defaults to `0.0000` | | `settlement_fee_total_ex_vat` | decimal | Defaults to `0.0000` | | `settlement_fee_total_incl_vat` | decimal | Defaults to `0.0000` | | `fee_currency_code` | string | ISO 4217. Empty when no fees apply | ## Matching a transaction [#matching-a-transaction] Match on `gateway_transaction_id`. It is the purchase identifier Precium returns when you create a purchase, so it is the one value that is stable from your API call through to settlement. Keep `transaction_rrn`, `transaction_auth_code` and the amount as secondary data. You will need them if a transaction has to be investigated at the acquiring bank, where the Precium identifier means nothing. **A refund does not carry its original purchase identifier in v3.0.** A refund is a separate payment transaction with its own `gateway_transaction_id`, its own RRN and its own authorisation code. Recon **v2.1** carried a `related_transaction_reference` column linking it back to the original purchase. **That column does not exist in v3.0.** If you need refund-to-purchase linkage, hold it on your side from the refund call, or ask your account contact which file version you are receiving. ### Granularity [#granularity] | Event | Rows | |---|---| | A purchase | One | | A hold followed by a capture | One. It is a single purchase | | A refund | Its own row, `transaction_type` `REFUND`, negative amount | | A chargeback | Its own row, `transaction_type` `CHARGEBACK`, negative amount | ## Column names across versions [#column-names-across-versions] A column changed name more than once. If you are reading an older file, or older documentation, this is the mapping. | v1.0 | v2.1 | v3.0 (current) | |---|---|---| | `purchase_id` | `gateway_transaction_id` | `gateway_transaction_id` | | `purchase_reference` | `gateway_transaction_reference` | `gateway_transaction_reference` | | `auth_code` | `auth_code` | `transaction_auth_code` | | `rrn` | `rrn` | `transaction_rrn` | | `is_3DS` | `three_ds` | `card_authenticated` | | `is_tokenized` | `tokenization` | `card_tokenized` | | `paid_at` | `transaction_date_time_utc` | `transaction_date_time_utc` | | `type` | `type` | `transaction_type` | | `related_to_purchase` | `related_transaction_reference` | *(removed)* | | `settlement_id` | `settlement_id` | *(removed)* | So `auth_code`, `transaction_auth_code` and the `authorisation_approval_code` you may see in other material are the same value under three names, from three different file versions. ## Delivery [#delivery] Files are delivered daily by SFTP. A captured transaction appears once it has been fully reconciled with the acquirer, which can be a few days after it was processed. Inclusion in the recon file means Precium has matched the transaction to acquiring bank data and considers it reconciled; reconciled transactions are then eligible for settlement under the agreed settlement schedule. # Get company balance. (/docs/reference/card/account/get-account-json-balance-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get company turnover. (/docs/reference/card/account/get-account-json-turnover-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get balance data. (/docs/reference/card/balance/get-balance-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a billing template by ID. (/docs/reference/card/billing/delete-billing-templates-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List all billing templates. (/docs/reference/card/billing/get-billing-templates-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a billing template by ID. (/docs/reference/card/billing/get-billing-templates-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List all billing template clients for this billing template. (/docs/reference/card/billing/get-billing-templates-id-clients-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a billing template client by client's ID. (/docs/reference/card/billing/get-billing-templates-id-clients-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a list of Purchases and Refunds for a Client. (/docs/reference/card/billing/get-billing-templates-id-clients-id-feed-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Partially update a billing template client by client's ID. (/docs/reference/card/billing/patch-billing-templates-id-clients-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Send an invoice to one or several clients. (/docs/reference/card/billing/post-billing-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a template to issue repeated invoices from in the future, with or without a subscription. (/docs/reference/card/billing/post-billing-templates-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Add a billing template client and activate recurring billing (is_subscription: true). (/docs/reference/card/billing/post-billing-templates-id-add-subscriber-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Send an invoice, generating a purchase from billing template data. (/docs/reference/card/billing/post-billing-templates-id-send-invoice-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update a billing template by ID. (/docs/reference/card/billing/put-billing-templates-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a client by ID. (/docs/reference/card/clients/delete-clients-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a client recurring token by ID. (/docs/reference/card/clients/delete-clients-id-recurring-tokens-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List all clients. (/docs/reference/card/clients/get-clients-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve an object by ID. (/docs/reference/card/clients/get-clients-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a list of Purchases and Refunds for a Client. (/docs/reference/card/clients/get-clients-id-feed-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List recurring tokens saved for a client. (/docs/reference/card/clients/get-clients-id-recurring-tokens-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve an object by ID. (/docs/reference/card/clients/get-clients-id-recurring-tokens-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Partially update a client by ID. (/docs/reference/card/clients/patch-clients-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new client. (/docs/reference/card/clients/post-clients-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update a client by ID. (/docs/reference/card/clients/put-clients-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List all generated statements. (/docs/reference/card/company-statements/get-company-statements-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve a statement by ID. (/docs/reference/card/company-statements/get-company-statements-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Schedule a statement generation. (/docs/reference/card/company-statements/post-company-statements-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Cancel a statement generation by ID. (/docs/reference/card/company-statements/post-company-statements-id-cancel-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Card API (/docs/reference/card) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} /` request; 3) set up a Webhook using the UI or Webhook API to listen to `purchase.paid` or `purchase.payment_failure` event on your server.\n\nUsing `skip_capture` flag allows you to separate the authentication and payment execution steps, allowing you to reserve funds on payer’s card account for some time. This flag can also enable preauthorization capability, allowing you to save the card without a financial transaction, if available.\n\nIn case making a purchase client agrees to store his card for the upcoming purchases, next time he will be able to pay in a single click.\n\nInstead of a redirect you can also utilize Direct Post checkout: you can create an HTML `
` on your website with `method=\"POST\"` and `action` pointing to `direct_post_url` of a created Purchase. You will also need to saturate form with ``-s for card data fields. As a result, when a payer submits their card data, it will be posted straight to our system, allowing you to customize the checkout as you wish while your PCI DSS requirement is only raised to SAQ A-EP, as your system doesn't receive or process card data. For more details, see the documentation on Purchase's `direct_post_url` field.\n\nTo pay for test Purchases, use `4444 3333 2222 1111` as the card number, `123` as CVC, any date/month greater than now as expiry and any (Latin) cardholder name. Any other card number/CVC/expiry not greater or equal than the current month will all fail a test payment. "} /> # Get the list of payment methods available for your purchase. (/docs/reference/card/payment-methods/get-payment-methods-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get the list of payout methods available for your payout. (/docs/reference/card/payout-methods/get-payout-methods-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve an object by ID. (/docs/reference/card/payouts/get-payouts-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create an unprocessed payout object. A separate request is needed to execute the payout. (/docs/reference/card/payouts/post-payouts-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get a callback public key. (/docs/reference/card/public-key/get-public-key-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve an object by ID. (/docs/reference/card/purchases/get-purchases-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a purchase – the main request for any e-commerce integration. (/docs/reference/card/purchases/post-purchases-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Cancel a pending purchase. (/docs/reference/card/purchases/post-purchases-id-cancel-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Capture a previously authorized payment. (/docs/reference/card/purchases/post-purchases-id-capture-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Charge or Hold a purchase using a saved token. (/docs/reference/card/purchases/post-purchases-id-charge-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a recurring token associated with a purchase. (/docs/reference/card/purchases/post-purchases-id-delete-recurring-token-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Mark a purchase as paid. (/docs/reference/card/purchases/post-purchases-id-mark-as-paid-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Refund a paid purchase. (/docs/reference/card/purchases/post-purchases-id-refund-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Release funds on hold. (/docs/reference/card/purchases/post-purchases-id-release-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Re-sends invoice (/docs/reference/card/purchases/post-purchases-id-resend-invoice-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a webhook by ID. (/docs/reference/card/webhooks/delete-webhooks-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List all webhooks. (/docs/reference/card/webhooks/get-webhooks-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List webhook deliveries for a specific object. (/docs/reference/card/webhooks/get-webhooks-deliveries-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Retrieve an object by ID. (/docs/reference/card/webhooks/get-webhooks-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Partially update a webhook by ID. (/docs/reference/card/webhooks/patch-webhooks-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new webhook. (/docs/reference/card/webhooks/post-webhooks-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update a webhook by ID. (/docs/reference/card/webhooks/put-webhooks-id-) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # /v2/accountverification/validate (/docs/reference/debit-orders/accountverification/post-v2-accountverification-validate) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # /v2/accountverification/verify (/docs/reference/debit-orders/accountverification/post-v2-accountverification-verify) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Markoff Details (/docs/reference/debit-orders/bankaccountmarkoff/get-v2-bankaccountmarkoff) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # List Bank Account Settlement executions (/docs/reference/debit-orders/bankaccountsettlement/get-v2-bankaccountsettlement-config-configid-execute) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets a specific Bank Account Statement (/docs/reference/debit-orders/bankaccountstatement/get-v2-bankaccountstatement-bankaccountnumber-id) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Bank Account Statements (/docs/reference/debit-orders/bankaccountstatement/get-v2-bankaccountstatement-bankaccountnumber) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Creates a Debicheck Collection Callback (/docs/reference/debit-orders/callback/post-v2-callback-collection-debicheck-collectionid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Creates a Eft Collection Callback (/docs/reference/debit-orders/callback/post-v2-callback-collection-eft-collectionid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Creates a Debicheck Mandate Callback (/docs/reference/debit-orders/callback/post-v2-callback-mandate-debicheck-mandateid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Creates a Eft Payment Callback (/docs/reference/debit-orders/callback/post-v2-callback-payment-eft-paymentid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Tests a Debicheck Collection Callback (/docs/reference/debit-orders/callback/post-v2-callback-test-collection-debicheck-collectionid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Tests a Eft Collection Callback (/docs/reference/debit-orders/callback/post-v2-callback-test-collection-eft-collectionid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Tests Debicheck Mandate Callback (/docs/reference/debit-orders/callback/post-v2-callback-test-mandate-debicheck-mandateid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Tests a Eft Payment Callback (/docs/reference/debit-orders/callback/post-v2-callback-test-payment-eft-paymentid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Tests a Settlement Callback (/docs/reference/debit-orders/callback/post-v2-callback-test-settlement-settlementid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a Client Callback URL (/docs/reference/debit-orders/clientcallbackurl/delete-v2-clientcallbackurl-id) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets a specific Client CallBack URL (/docs/reference/debit-orders/clientcallbackurl/get-v2-clientcallbackurl-id) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Client Callback URLs (/docs/reference/debit-orders/clientcallbackurl/get-v2-clientcallbackurl) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new Client CallbackUrl (/docs/reference/debit-orders/clientcallbackurl/post-v2-clientcallbackurl) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update a Client Callback Url (/docs/reference/debit-orders/clientcallbackurl/put-v2-clientcallbackurl-id) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get a specific Profile for a specific client (/docs/reference/debit-orders/clientprofile/get-v2-clientprofile-id) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Profiles for a specific client (/docs/reference/debit-orders/clientprofile/get-v2-clientprofile) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a Debicheck Collection (/docs/reference/debit-orders/collection/delete-v2-collection-debicheck-collectionid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a EFT Collection (/docs/reference/debit-orders/collection/delete-v2-collection-eft-collectionid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets the latest status of a Debicheck Collection (/docs/reference/debit-orders/collection/get-v2-collection-debicheck-collectionid-status) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets specific Debicheck Collection (/docs/reference/debit-orders/collection/get-v2-collection-debicheck-collectionid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Download previously processed csv results after uploading debicheck collections (/docs/reference/debit-orders/collection/get-v2-collection-debicheck-download-csv-bulkuploadid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Generate a csv template to upload debicheck collections (/docs/reference/debit-orders/collection/get-v2-collection-debicheck-upload-csv-generate-template) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Debicheck Collection (/docs/reference/debit-orders/collection/get-v2-collection-debicheck) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets the latest status of a EFT Collection (/docs/reference/debit-orders/collection/get-v2-collection-eft-collectionid-status) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets specific EFT Collection (/docs/reference/debit-orders/collection/get-v2-collection-eft-collectionid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Download previously processed csv results after uploading eft collections (/docs/reference/debit-orders/collection/get-v2-collection-eft-download-csv-bulkuploadid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Generate a csv template to upload eft collections (/docs/reference/debit-orders/collection/get-v2-collection-eft-upload-csv-generate-template) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all EFT Collection (/docs/reference/debit-orders/collection/get-v2-collection-eft) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Recreate a Debicheck Collection as a new EFT Collection (/docs/reference/debit-orders/collection/post-v2-collection-debicheck-collectionid-recreate-as-eft) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new Debicheck Collection from an existing Debicheck Mandate (/docs/reference/debit-orders/collection/post-v2-collection-debicheck-from-mandate) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create debicheck collections from a csv template (/docs/reference/debit-orders/collection/post-v2-collection-debicheck-upload-csv) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new Debicheck Collection (/docs/reference/debit-orders/collection/post-v2-collection-debicheck) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new EFT Collection from an existing EFT Mandate (/docs/reference/debit-orders/collection/post-v2-collection-eft-from-mandate) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create eft collections from a csv template (/docs/reference/debit-orders/collection/post-v2-collection-eft-upload-csv) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new EFT Mandate and Collection based on the created mandate (/docs/reference/debit-orders/collection/post-v2-collection-eft) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Collection Results (/docs/reference/debit-orders/collectionresult/get-v2-collectionresult) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all collection events, ordered by the time they were created at (/docs/reference/debit-orders/event/get-v2-event-collection) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all mandate events, ordered by the time they were created at (/docs/reference/debit-orders/event/get-v2-event-mandate) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all payment events, ordered by the time they were created at (/docs/reference/debit-orders/event/get-v2-event-payment) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all profile events, ordered by the time they were created at (/docs/reference/debit-orders/event/get-v2-event-profile) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Debit order API (/docs/reference/debit-orders) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete an EFT Mandate (/docs/reference/debit-orders/mandate/delete-v2-mandate-eft-mandateid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Download previously processed csv results after uploading debicheck mandates (/docs/reference/debit-orders/mandate/get-v2-mandate-debicheck-download-csv-bulkuploadid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets the latest status of a Debicheck Mandate (/docs/reference/debit-orders/mandate/get-v2-mandate-debicheck-mandateid-status) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets specific Debicheck Mandate (/docs/reference/debit-orders/mandate/get-v2-mandate-debicheck-mandateid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Generate a csv template to upload debicheck mandates (/docs/reference/debit-orders/mandate/get-v2-mandate-debicheck-upload-csv-generate-template) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Debicheck Mandates (/docs/reference/debit-orders/mandate/get-v2-mandate-debicheck) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Download previously processed csv results after uploading eft mandates (/docs/reference/debit-orders/mandate/get-v2-mandate-eft-download-csv-bulkuploadid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets the latest status of a EFT Mandate (/docs/reference/debit-orders/mandate/get-v2-mandate-eft-mandateid-status) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets specific EFT Mandate (/docs/reference/debit-orders/mandate/get-v2-mandate-eft-mandateid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Generate a csv template to upload eft mandates (/docs/reference/debit-orders/mandate/get-v2-mandate-eft-upload-csv-generate-template) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all EFT Mandates (/docs/reference/debit-orders/mandate/get-v2-mandate-eft) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Patch a Debicheck Mandate (/docs/reference/debit-orders/mandate/patch-v2-mandate-debicheck-mandateid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Patch a EFT Mandate (/docs/reference/debit-orders/mandate/patch-v2-mandate-eft-mandateid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # 'requiresAuth' is set internally, please do not override this value unless manual intervention is required Amends a Specific Mandate at the bank (/docs/reference/debit-orders/mandate/post-v2-mandate-debicheck-mandateid-amend) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Authenticates a Specific Mandate (/docs/reference/debit-orders/mandate/post-v2-mandate-debicheck-mandateid-auth) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Starts polling a debicheck mandate result at the bank and potentially change the state. (/docs/reference/debit-orders/mandate/post-v2-mandate-debicheck-mandateid-poll) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Resets a debicheck mandate to the latest previously completed state. (/docs/reference/debit-orders/mandate/post-v2-mandate-debicheck-mandateid-reset) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create debicheck mandates from a csv template (/docs/reference/debit-orders/mandate/post-v2-mandate-debicheck-upload-csv) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new Debicheck Mandate (/docs/reference/debit-orders/mandate/post-v2-mandate-debicheck) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create eft mandates from a csv template (/docs/reference/debit-orders/mandate/post-v2-mandate-eft-upload-csv) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new EFT Mandate (/docs/reference/debit-orders/mandate/post-v2-mandate-eft) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update a Debicheck Mandate (/docs/reference/debit-orders/mandate/put-v2-mandate-debicheck-mandateid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Update an EFT Mandate (/docs/reference/debit-orders/mandate/put-v2-mandate-eft-mandateid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a Mandate Audit (/docs/reference/debit-orders/mandateaudit/delete-v2-mandateaudit-id) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get a specific Mandate Audit (/docs/reference/debit-orders/mandateaudit/get-v2-mandateaudit-id) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Mandate Audits or all profiles for a specific client (/docs/reference/debit-orders/mandateaudit/get-v2-mandateaudit) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new Mandate Audit (/docs/reference/debit-orders/mandateaudit/post-v2-mandateaudit) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Mandate Results (/docs/reference/debit-orders/mandateresult/get-v2-mandateresult) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new Mandate Result (/docs/reference/debit-orders/mandateresult/post-v2-mandateresult) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets a specific Merchant Profile (/docs/reference/debit-orders/merchantprofile/get-v2-merchantprofile-id) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Merchant Profiles (/docs/reference/debit-orders/merchantprofile/get-v2-merchantprofile) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Delete a Eft Payment (/docs/reference/debit-orders/payment/delete-v2-payment-eft-paymentid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Download previously processed csv results after uploading eft payments (/docs/reference/debit-orders/payment/get-v2-payment-eft-download-csv-bulkuploadid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets the latest status of a Eft Payment (/docs/reference/debit-orders/payment/get-v2-payment-eft-paymentid-status) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Gets specific Eft Payment (/docs/reference/debit-orders/payment/get-v2-payment-eft-paymentid) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Generate a csv template to upload eft payments (/docs/reference/debit-orders/payment/get-v2-payment-eft-upload-csv-generate-template) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Eft Payments (/docs/reference/debit-orders/payment/get-v2-payment-eft) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create eft payments from a csv template (/docs/reference/debit-orders/payment/post-v2-payment-eft-upload-csv) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Create a new Eft Payment (/docs/reference/debit-orders/payment/post-v2-payment-eft) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all reports per profile (/docs/reference/debit-orders/reporting/get-v2-reporting) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Get a specific Transaction File (/docs/reference/debit-orders/transactionfile/get-v2-transactionfile-id) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # Lists all Transaction Files (/docs/reference/debit-orders/transactionfile/get-v2-transactionfile) {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} # API reference (/docs/reference) Precium exposes two systems; which reference you want depends on which one you're integrating against (see [Overview](/docs/overview) if you're not sure). }> Purchases, payouts, billing, clients and webhooks on the card gateway. Used by both Server-to-Server and Payment Orchestration. }> Mandates, collections and payments on the Direct Debit (Mandates) API, a separate system on its own base URL. # Advanced refund management (/docs/server-to-server/advanced-refund-management) ## Overview [#overview] How refunds behave on the card API: what can be refunded, how much, how partial refunds accumulate, and what happens when one fails. One rule shapes everything below: **a refund cannot be voided.** Once submitted it runs to completion. There is no reversal operation, so the decision to refund is final at the point you make the call. ## Before you refund — check availability [#before-you-refund--check-availability] Refundability is a property of the purchase, not a global rule. Read it before offering a refund in your own interface. ```bash GET https://gate.reviopay.com/api/v1/purchases/{id}/ ``` | Field | Meaning | |---|---| | `refund_availability` | What kind of refund this purchase permits | | `refundable_amount` | How much is still available to refund | ### `refund_availability` values [#refund_availability-values] | Value | Meaning | |---|---| | `all` | Full and partial refunds both permitted | | `full_only` | The whole amount, or nothing | | `partial_only` | Partial refunds only | | `pis_all` | Full and partial, via payment initiation | | `pis_partial` | Partial only, via payment initiation | | `none` | Not refundable through the API | Do not assume `all`. Refundability varies by payment method, and a purchase paid by a method that cannot be refunded programmatically will return `none` — in which case the refund has to be handled off-platform. ## Full refund [#full-refund] Omit `amount` and the entire remaining balance is refunded. ```bash curl -X POST https://gate.reviopay.com/api/v1/purchases/$PURCHASE_ID/refund/ \ -H "Authorization: Bearer $PRECIUM_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` ## Partial refund [#partial-refund] Send `amount` as an integer in cents. ```bash curl -X POST https://gate.reviopay.com/api/v1/purchases/$PURCHASE_ID/refund/ \ -H "Authorization: Bearer $PRECIUM_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 50000 }' ``` **Partial refunds may be repeated** until the original total is exhausted. Each one reduces `refundable_amount`; when it reaches zero the purchase is fully refunded and further attempts fail. ### Worked example [#worked-example] | Step | Request | refundable_amount after | |---|---|---| | Purchase paid | — | 149900 | | Refund one item | `{ "amount": 49900 }` | 100000 | | Refund shipping | `{ "amount": 10000 }` | 90000 | | Refund the balance | `{}` | 0 | Always read `refundable_amount` back rather than tracking it yourself. Chargebacks and reversals can change it without your involvement. ## A refund generates a payment object [#a-refund-generates-a-payment-object] A successful refund returns a **Payment** object, not a modified purchase. Store its identifier against your own order record — it is what reconciliation reports and dispute correspondence will reference. ## Asynchronous behaviour [#asynchronous-behaviour] Refunds are not always immediate. If the acquirer takes too long you receive HTTP 200 with the purchase in `status: pending_refund`, together with a `purchase.pending_refund` webhook. | Signal | Meaning | |---|---| | `status: pending_refund` | Accepted, still processing at the acquirer | | `payment.refunded` webhook | Completed. Carries the Payment generated by the refund | | `purchase.refund_failure` webhook | Failed after acceptance | | `status: refunded` | Fully refunded | **Subscribe to `payment.refunded` and treat it as the completion signal.** A 200 on the refund call means accepted, not settled. ## When a refund fails [#when-a-refund-fails] A processing failure returns HTTP 400 with error code `purchase_refund_error`. The response itself does not carry the reason — retrieve it: ```bash GET https://gate.reviopay.com/api/v1/purchases/{id}/ ``` Read `transaction_data.attempts[]`, newest element first. The matching attempt's `.error` carries the code and description. ### Common causes [#common-causes] | Cause | What to do | |---|---| | Amount exceeds `refundable_amount` | Read the current value and resubmit within it | | Partial refund on a `full_only` purchase | Refund the full amount instead | | `refund_availability: none` | Handle off-platform; contact support | | Purchase not in a refundable state | An authorisation on hold is *released*, not refunded | | Acquirer or issuer rejection | Retry later; if it persists, contact support with the purchase id | ## Refund, release or cancel? [#refund-release-or-cancel] Three different operations, frequently confused. The purchase's current status decides which applies. | Purchase status | Operation | Effect | |---|---|---| | `created` — never paid | `POST /purchases/{id}/cancel/` | Cancels the purchase | | `hold` — authorised, not captured | `POST /purchases/{id}/release/` | Releases the reserved funds. No refund involved | | `paid` — captured | `POST /purchases/{id}/refund/` | Returns funds to the cardholder | Releasing a hold is materially better for the customer than capturing and refunding: the funds are never taken, so nothing has to travel back. Where you know a charge will not proceed, release rather than capture-then-refund. ## Refunds are not reversible [#refunds-are-not-reversible] There is no void or cancel operation for a refund. Practical consequences: - **Confirm before submitting.** Put the confirmation step in your own interface — the API will not give you a second chance - **A refund sent in error can only be recovered by charging the customer again**, which needs their consent and a fresh authorisation. Their stored token, if you hold one, makes this possible but does not make it automatic - **Guard against double submission.** There is no idempotency key, so a retried request is a second refund. Disable the control after the first click and reconcile against `refundable_amount` before retrying ## Chargebacks are not refunds [#chargebacks-are-not-refunds] A chargeback is initiated by the cardholder through their bank, not by you. It arrives as an event and moves the purchase to `status: chargeback`. | Event | Meaning | |---|---| | `payment.charged_back` | A chargeback has been raised against the payment | | `payment.chargeback_reversed` | The chargeback was reversed in your favour | Do not refund a purchase that has been charged back — you would return the funds twice. Check the status first. Dispute handling is a scheme process, not an API one; contact support on receiving a chargeback event. ## Reconciliation [#reconciliation] Refunds appear in reconciliation reports as separate entries referencing the original purchase. Match on the Payment identifier returned by the refund rather than on the purchase alone, since a purchase may carry several partial refunds. Formats are in *Reconciliation*. ## Checklist before going live with refunds [#checklist-before-going-live-with-refunds] 1. Read `refund_availability` before showing a refund control 2. Read `refundable_amount` rather than tracking balances yourself 3. Send amounts as integers in cents 4. Subscribe to `payment.refunded` and `purchase.refund_failure` 5. Treat HTTP 200 as accepted, not completed 6. Prevent double submission — there is no idempotency key 7. Check for `status: chargeback` before refunding 8. Use `/release/` for holds and `/cancel/` for unpaid purchases # Code examples (/docs/server-to-server/code-examples) ### Introduction [#introduction] This document provides production-ready code examples in multiple programming languages for integrating with Precium's Server-to-Server API. **Installation** ```bash pip install requests ``` **Complete Integration Class** ```python """ Precium S2S Integration - Python Requires: requests """ import requests from typing import Optional, Dict, Any from dataclasses import dataclass from enum import Enum import json class PreciumEnvironment(Enum): SANDBOX = "https://gate.reviopay.com/api/v1" PRODUCTION = "https://gate.reviopay.com/api/v1" @dataclass class CardData: cardholder_name: str card_number: str expires: str # MM/YY format cvc: str remote_ip: str @dataclass class BrowserFingerprint: user_agent: str accept_header: str language: str java_enabled: bool javascript_enabled: bool color_depth: int utc_offset: int screen_width: int screen_height: int class PreciumS2SClient: """Precium Server-to-Server API Client""" def __init__( self, standard_api_key: str, s2s_api_key: str, brand_id: str, environment: PreciumEnvironment = PreciumEnvironment.SANDBOX ): self.standard_api_key = standard_api_key self.s2s_api_key = s2s_api_key self.brand_id = brand_id self.base_url = environment.value def _headers(self, use_s2s: bool = False) -> Dict[str, str]: """Generate request headers with appropriate API key.""" api_key = self.s2s_api_key if use_s2s else self.standard_api_key return { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "Accept": "application/json" } def _request( self, method: str, endpoint: str, data: Optional[Dict] = None, use_s2s: bool = False ) -> Dict[str, Any]: """Make an API request.""" url = f"{self.base_url}{endpoint}" response = requests.request( method=method, url=url, headers=self._headers(use_s2s), json=data ) response.raise_for_status() return response.json() # ========================================================================= # Client Management # ========================================================================= def create_client(self, email: str, phone: Optional[str] = None) -> Dict: """ Create a new client record. Args: email: Customer's email address phone: Optional phone number in international format Returns: Client object with ID """ payload = {"email": email} if phone: payload["phone"] = phone return self._request("POST", "/clients/", payload) def get_client(self, client_id: str) -> Dict: """Retrieve a client by ID.""" return self._request("GET", f"/clients/{client_id}/") # ========================================================================= # Purchase Management # ========================================================================= def create_purchase( self, client_id: str, amount_cents: int, currency: str, product_name: str, success_redirect: str, failure_redirect: str, force_recurring: bool = False, metadata: Optional[Dict] = None ) -> Dict: """ Create a new purchase. Args: client_id: The client's ID amount_cents: Amount in cents (e.g., 29900 for R299.00) currency: ISO 4217 currency code (e.g., "ZAR") product_name: Description of the product/service success_redirect: URL for successful payment redirect failure_redirect: URL for failed payment redirect force_recurring: Set True to generate token for future MIT metadata: Optional custom key-value data Returns: Purchase object with direct_post_url """ payload = { "client_id": client_id, "brand_id": self.brand_id, "currency": currency, "products": [{"name": product_name, "price": amount_cents}], "success_redirect": success_redirect, "failure_redirect": failure_redirect, } if force_recurring: payload["force_recurring"] = True if metadata: payload["metadata"] = metadata return self._request("POST", "/purchases/", payload) def create_mit_purchase( self, client_id: str, amount_cents: int, currency: str, product_name: str, card_token: str, network_transaction_id: str, original_amount_cents: int ) -> Dict: """ Create a purchase for Merchant-Initiated Transaction (recurring). Args: client_id: The client's ID amount_cents: Amount in cents currency: ISO 4217 currency code product_name: Description of the charge card_token: Token from original CIT transaction network_transaction_id: Network transaction ID from original original_amount_cents: Amount from original tokenization Returns: Purchase object ready for MIT charge """ payload = { "client_id": client_id, "brand_id": self.brand_id, "currency": currency, "products": [{"name": product_name, "price": amount_cents}], "payment_method_details": { "card_token": card_token, "previous_network_transaction_id": network_transaction_id, "original_amount_cents": original_amount_cents }, "is_recurring": True } return self._request("POST", "/purchases/", payload) def get_purchase(self, purchase_id: str) -> Dict: """Retrieve purchase details by ID.""" return self._request("GET", f"/purchases/{purchase_id}/") # ========================================================================= # Card Data Submission (Direct Post) # ========================================================================= def submit_card_data( self, direct_post_url: str, card: CardData, fingerprint: BrowserFingerprint, remember_card: bool = False ) -> Dict: """ Submit card data to the direct post URL. This method handles the S2S card submission for CIT with internal 3DS. Args: direct_post_url: The URL from purchase creation card: Card details fingerprint: Browser fingerprint data for 3DS remember_card: Whether to tokenize the card Returns: Response containing 3DS parameters or ready status """ payload = { "cardholder_name": card.cardholder_name, "card_number": card.card_number, "expires": card.expires, "cvc": card.cvc, "remember_card": str(remember_card).lower(), "remote_ip": card.remote_ip, "user_agent": fingerprint.user_agent, "accept_header": fingerprint.accept_header, "language": fingerprint.language, "java_enabled": str(fingerprint.java_enabled).lower(), "javascript_enabled": str(fingerprint.javascript_enabled).lower(), "color_depth": str(fingerprint.color_depth), "utc_offset": str(fingerprint.utc_offset), "screen_width": str(fingerprint.screen_width), "screen_height": str(fingerprint.screen_height), } headers = { "Authorization": f"Bearer {self.s2s_api_key}", "Content-Type": "application/x-www-form-urlencoded" } response = requests.post( direct_post_url, headers=headers, data=payload ) response.raise_for_status() return response.json() def submit_token_for_mit( self, direct_post_url: str, card_token: str, server_ip: str ) -> Dict: """ Submit token for MIT transaction. Args: direct_post_url: The URL from MIT purchase creation card_token: The stored card token server_ip: Your server's IP address Returns: Response indicating ready for charge """ payload = { "card_token": card_token, "remote_ip": server_ip } headers = { "Authorization": f"Bearer {self.s2s_api_key}", "Content-Type": "application/x-www-form-urlencoded" } response = requests.post( direct_post_url, headers=headers, data=payload ) response.raise_for_status() return response.json() # ========================================================================= # Charge and Refund # ========================================================================= def charge_purchase(self, purchase_id: str) -> Dict: """ Finalize the transaction and charge the card. Call this after 3DS completion or for MIT transactions. Args: purchase_id: The purchase ID to charge Returns: Transaction result with payment details and token """ return self._request("POST", f"/purchases/{purchase_id}/charge/") def refund_purchase( self, purchase_id: str, amount_cents: Optional[int] = None ) -> Dict: """ Process a refund for a purchase. Args: purchase_id: The purchase ID to refund amount_cents: Amount to refund in cents (None for full refund) Returns: Refund confirmation """ payload = {} if amount_cents: payload["amount"] = amount_cents return self._request("POST", f"/purchases/{purchase_id}/refund/", payload) # ========================================================================= # Pre-Authorization Operations # ========================================================================= def capture_purchase( self, purchase_id: str, amount_cents: Optional[int] = None ) -> Dict: """ Capture a pre-authorized transaction. Args: purchase_id: The purchase ID to capture amount_cents: Amount to capture in cents (None for full capture) Returns: Capture confirmation """ payload = {} if amount_cents: payload["amount"] = amount_cents return self._request("POST", f"/purchases/{purchase_id}/capture/", payload) def void_purchase(self, purchase_id: str) -> Dict: """ Void/cancel a pre-authorized or captured transaction (before settlement). Args: purchase_id: The purchase ID to void Returns: Void confirmation """ return self._request("POST", f"/purchases/{purchase_id}/cancel/") def void_refund(self, purchase_id: str, refund_id: str) -> Dict: """ Void a refund before settlement. Args: purchase_id: The original purchase ID refund_id: The refund ID to void Returns: Void confirmation """ return self._request("POST", f"/purchases/{purchase_id}/refund/{refund_id}/cancel/") # ========================================================================= # Zero Authorization (Card Validation) # ========================================================================= def create_zero_auth_purchase( self, client_id: str, currency: str, success_redirect: str, failure_redirect: str, force_recurring: bool = True ) -> Dict: """ Create a zero-amount authorization for card validation. Args: client_id: The client's ID currency: ISO 4217 currency code success_redirect: URL for successful validation failure_redirect: URL for failed validation force_recurring: Set True to tokenize the card Returns: Purchase object for zero authorization """ payload = { "client_id": client_id, "brand_id": self.brand_id, "currency": currency, "products": [{"name": "Card Validation", "price": 0}], "success_redirect": success_redirect, "failure_redirect": failure_redirect, } if force_recurring: payload["force_recurring"] = True return self._request("POST", "/purchases/", payload) # ========================================================================= # External 3DS (MPI) # ========================================================================= def create_external_3ds_purchase( self, client_id: str, amount_cents: int, currency: str, product_name: str, success_redirect: str, failure_redirect: str, authentication_transaction_id: str, cavv: str, eci: str, xid: Optional[str] = None, ds_trans_id: Optional[str] = None ) -> Dict: """ Create a purchase with external 3DS authentication. Args: client_id: The client's ID amount_cents: Amount in cents currency: ISO 4217 currency code product_name: Description of product/service success_redirect: URL for successful payment failure_redirect: URL for failed payment authentication_transaction_id: Your MPI's transaction ID cavv: Cardholder Authentication Verification Value eci: Electronic Commerce Indicator xid: Transaction ID (3DS1 only) ds_trans_id: Directory Server Transaction ID (3DS2 only) Returns: Purchase object with direct_post_url """ payment_details = { "card": { "is_external_3DS": True, "authentication_transaction_id": authentication_transaction_id, "cavv": cavv, "eci_raw": eci } } if xid: payment_details["card"]["xid"] = xid if ds_trans_id: payment_details["card"]["ds_trans_id"] = ds_trans_id payload = { "client_id": client_id, "brand_id": self.brand_id, "currency": currency, "products": [{"name": product_name, "price": amount_cents}], "payment_method_details": payment_details, "success_redirect": success_redirect, "failure_redirect": failure_redirect, } return self._request("POST", "/purchases/", payload) # ============================================================================= # Usage Examples # ============================================================================= def example_cit_with_internal_3ds(): """Example: Customer-Initiated Transaction with Internal 3DS""" client = PreciumS2SClient( standard_api_key='your_standard_api_key', s2s_api_key='your_s2s_api_key', brand_id='your_brand_id' ) # Step 1: Create client customer = client.create_client(email='customer@example.com') print(f"Created client: {customer['id']}") # Step 2: Create purchase purchase = client.create_purchase( client_id=customer['id'], amount_cents=29900, currency='ZAR', product_name='Premium Subscription', success_redirect='https://yoursite.com/success', failure_redirect='https://yoursite.com/failure', force_recurring=True # Generate token for future recurring ) print(f"Created purchase: {purchase['id']}") print(f"Direct post URL: {purchase['direct_post_url']}") # Step 3: Submit card data card = CardData( cardholder_name='John Smith', card_number='4000000000001091', expires='12/28', cvc='123', remote_ip='196.21.45.123' ) fingerprint = BrowserFingerprint( user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64)', accept_header='text/html,application/xhtml+xml', language='en-ZA', java_enabled=False, javascript_enabled=True, color_depth=24, utc_offset=-120, screen_width=1920, screen_height=1080 ) result = client.submit_card_data( direct_post_url=purchase['direct_post_url'], card=card, fingerprint=fingerprint, remember_card=True ) # Step 4: Handle 3DS (in production, redirect customer) if '3ds' in result: print("3DS authentication required") print(f"3DS URL: {result['3ds']['3ds_url']}") print(f"MD: {result['3ds']['MD']}") # Redirect customer to 3DS URL... # Step 5: After 3DS completion, charge charge_result = client.charge_purchase(purchase['id']) print(f"Charge result: {charge_result['status']}") if charge_result['status'] == 'paid': # Store token for future MIT token = charge_result['payment_method_details']['card']['token'] network_txn_id = charge_result['payment_method_details']['card']['network_transaction_id'] print(f"Token for recurring: {token}") print(f"Network Transaction ID: {network_txn_id}") def example_mit_recurring(): """Example: Merchant-Initiated Recurring Transaction""" client = PreciumS2SClient( standard_api_key='your_standard_api_key', s2s_api_key='your_s2s_api_key', brand_id='your_brand_id' ) # Stored from original CIT transaction stored_token = "tok_abc123def456..." stored_network_txn_id = "MCC123456789012345" original_amount = 29900 # Create MIT purchase purchase = client.create_mit_purchase( client_id='existing_client_id', amount_cents=29900, currency='ZAR', product_name='Monthly Subscription - February 2026', card_token=stored_token, network_transaction_id=stored_network_txn_id, original_amount_cents=original_amount ) # Submit token client.submit_token_for_mit( direct_post_url=purchase['direct_post_url'], card_token=stored_token, server_ip='10.0.0.1' # Your server IP ) # Charge result = client.charge_purchase(purchase['id']) print(f"MIT charge result: {result['status']}") def example_zero_authorization(): """Example: Zero Authorization for Card Validation""" client = PreciumS2SClient( standard_api_key='your_standard_api_key', s2s_api_key='your_s2s_api_key', brand_id='your_brand_id' ) # Create client customer = client.create_client(email='customer@example.com') # Create zero-amount purchase for card validation purchase = client.create_zero_auth_purchase( client_id=customer['id'], currency='ZAR', success_redirect='https://yoursite.com/validation/success', failure_redirect='https://yoursite.com/validation/failure', force_recurring=True # Tokenize for future use ) print(f"Zero auth purchase: {purchase['id']}") # Submit card data (same as standard flow) card = CardData( cardholder_name='John Smith', card_number='4000000000001091', expires='12/28', cvc='123', remote_ip='196.21.45.123' ) fingerprint = BrowserFingerprint( user_agent='Mozilla/5.0', accept_header='text/html', language='en-ZA', java_enabled=False, javascript_enabled=True, color_depth=24, utc_offset=-120, screen_width=1920, screen_height=1080 ) result = client.submit_card_data( direct_post_url=purchase['direct_post_url'], card=card, fingerprint=fingerprint, remember_card=True ) # Handle 3DS if required, then charge to validate # Card is validated without any charge def example_pre_authorization(): """Example: Pre-Authorization with Later Capture""" client = PreciumS2SClient( standard_api_key='your_standard_api_key', s2s_api_key='your_s2s_api_key', brand_id='your_brand_id' ) # Create client customer = client.create_client(email='guest@hotel.com') # Pre-authorize for hotel stay (estimated amount) purchase = client.create_purchase( client_id=customer['id'], amount_cents=500000, # R5,000 estimated currency='ZAR', product_name='Hotel Stay - 3 nights (estimated)', success_redirect='https://hotel.com/booking/confirmed', failure_redirect='https://hotel.com/booking/failed', force_recurring=False ) # Submit card data and complete 3DS (same as standard flow) # ... card submission code ... # Later: Guest checks out, actual bill is R4,500 # Partial capture capture_result = client.capture_purchase( purchase_id=purchase['id'], amount_cents=450000 # Capture only R4,500 ) print(f"Captured: {capture_result['status']}") # Alternative: Guest cancels before checkout # Void the pre-authorization # void_result = client.void_purchase(purchase['id']) # print(f"Voided: {void_result['status']}") def example_partial_refund(): """Example: Partial Refund Flow""" client = PreciumS2SClient( standard_api_key='your_standard_api_key', s2s_api_key='your_s2s_api_key', brand_id='your_brand_id' ) # Assume we have a completed purchase purchase_id = "existing_paid_purchase_id" # Customer returns one item worth R100 refund_result = client.refund_purchase( purchase_id=purchase_id, amount_cents=10000 # R100 partial refund ) print(f"Partial refund: {refund_result['status']}") print(f"Refund ID: {refund_result.get('refund_id')}") # Can issue multiple partial refunds up to original amount def example_external_3ds(): """Example: External 3DS (MPI) Flow""" client = PreciumS2SClient( standard_api_key='your_standard_api_key', s2s_api_key='your_s2s_api_key', brand_id='your_brand_id' # Must be Non-3DS brand ) # Create client customer = client.create_client(email='customer@example.com') # After completing 3DS with your MPI, create purchase with auth data purchase = client.create_external_3ds_purchase( client_id=customer['id'], amount_cents=50000, currency='ZAR', product_name='External 3DS Purchase', success_redirect='https://yoursite.com/success', failure_redirect='https://yoursite.com/failure', authentication_transaction_id='GAux5DosxbhI35RL8nc0', cavv='AAABBBCCCdddEEEfff111222333=', eci='05', ds_trans_id='a1b2c3d4-e5f6-7890-abcd-ef1234567890' # 3DS2 ) # Submit card data (no browser fingerprint needed) card = CardData( cardholder_name='John Smith', card_number='4000000000001091', expires='12/28', cvc='000', # Can use placeholder for external 3DS remote_ip='8.8.8.8' ) # ... submit card and charge ... if __name__ == "__main__": example_cit_with_internal_3ds() ``` **Installation** ```bash npm install axios ``` **Complete Integration Module** ```jsx /** * Precium S2S Integration - Node.js * Requires: axios */ const axios = require('axios'); const ENVIRONMENTS = { SANDBOX: 'https://gate.reviopay.com/api/v1', PRODUCTION: 'https://gate.reviopay.com/api/v1' }; class PreciumS2SClient { /** * Initialize the Precium S2S client * @param {Object} config Configuration object * @param {string} config.standardApiKey Standard API key for general requests * @param {string} config.s2sApiKey S2S API key for card data submission * @param {string} config.brandId Your brand ID * @param {string} [config.environment='SANDBOX'] Environment to use */ constructor({ standardApiKey, s2sApiKey, brandId, environment = 'SANDBOX' }) { this.standardApiKey = standardApiKey; this.s2sApiKey = s2sApiKey; this.brandId = brandId; this.baseUrl = ENVIRONMENTS[environment]; } /** * Get headers for API requests * @private */ _getHeaders(useS2S = false) { const apiKey = useS2S ? this.s2sApiKey : this.standardApiKey; return { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Accept': 'application/json' }; } /** * Make an API request * @private */ async _request(method, endpoint, data = null, useS2S = false) { const url = `${this.baseUrl}${endpoint}`; const response = await axios({ method, url, headers: this._getHeaders(useS2S), data }); return response.data; } // =========================================================================== // Client Management // =========================================================================== /** * Create a new client record * @param {string} email Customer's email address * @param {string} [phone] Optional phone number * @returns {Promise} Client object with ID */ async createClient(email, phone = null) { const payload = { email }; if (phone) payload.phone = phone; return this._request('POST', '/clients/', payload); } /** * Get client by ID * @param {string} clientId Client ID * @returns {Promise} Client object */ async getClient(clientId) { return this._request('GET', `/clients/${clientId}/`); } // =========================================================================== // Purchase Management // =========================================================================== /** * Create a new purchase * @param {Object} options Purchase options * @returns {Promise} Purchase object with direct_post_url */ async createPurchase({ clientId, amountCents, currency, productName, successRedirect, failureRedirect, forceRecurring = false, metadata = null }) { const payload = { client_id: clientId, brand_id: this.brandId, currency, products: [{ name: productName, price: amountCents }], success_redirect: successRedirect, failure_redirect: failureRedirect }; if (forceRecurring) payload.force_recurring = true; if (metadata) payload.metadata = metadata; return this._request('POST', '/purchases/', payload); } /** * Create MIT (recurring) purchase * @param {Object} options MIT purchase options * @returns {Promise} Purchase object */ async createMITPurchase({ clientId, amountCents, currency, productName, cardToken, networkTransactionId, originalAmountCents }) { const payload = { client_id: clientId, brand_id: this.brandId, currency, products: [{ name: productName, price: amountCents }], payment_method_details: { card_token: cardToken, previous_network_transaction_id: networkTransactionId, original_amount_cents: originalAmountCents }, is_recurring: true }; return this._request('POST', '/purchases/', payload); } /** * Get purchase by ID * @param {string} purchaseId Purchase ID * @returns {Promise} Purchase object */ async getPurchase(purchaseId) { return this._request('GET', `/purchases/${purchaseId}/`); } // =========================================================================== // Card Data Submission // =========================================================================== /** * Submit card data to direct post URL * @param {string} directPostUrl URL from purchase creation * @param {Object} cardData Card details * @param {Object} fingerprint Browser fingerprint * @param {boolean} [rememberCard=false] Whether to tokenize * @returns {Promise} 3DS parameters or ready status */ async submitCardData(directPostUrl, cardData, fingerprint, rememberCard = false) { const formData = new URLSearchParams({ cardholder_name: cardData.cardholderName, card_number: cardData.cardNumber, expires: cardData.expires, cvc: cardData.cvc, remember_card: rememberCard.toString(), remote_ip: cardData.remoteIp, user_agent: fingerprint.userAgent, accept_header: fingerprint.acceptHeader, language: fingerprint.language, java_enabled: fingerprint.javaEnabled.toString(), javascript_enabled: fingerprint.javascriptEnabled.toString(), color_depth: fingerprint.colorDepth.toString(), utc_offset: fingerprint.utcOffset.toString(), screen_width: fingerprint.screenWidth.toString(), screen_height: fingerprint.screenHeight.toString() }); const response = await axios.post(directPostUrl, formData.toString(), { headers: { 'Authorization': `Bearer ${this.s2sApiKey}`, 'Content-Type': 'application/x-www-form-urlencoded' } }); return response.data; } /** * Submit token for MIT transaction * @param {string} directPostUrl URL from MIT purchase * @param {string} cardToken Stored card token * @param {string} serverIp Your server's IP * @returns {Promise} Ready status */ async submitTokenForMIT(directPostUrl, cardToken, serverIp) { const formData = new URLSearchParams({ card_token: cardToken, remote_ip: serverIp }); const response = await axios.post(directPostUrl, formData.toString(), { headers: { 'Authorization': `Bearer ${this.s2sApiKey}`, 'Content-Type': 'application/x-www-form-urlencoded' } }); return response.data; } // =========================================================================== // Charge and Refund // =========================================================================== /** * Charge a purchase * @param {string} purchaseId Purchase ID to charge * @returns {Promise} Transaction result */ async chargePurchase(purchaseId) { return this._request('POST', `/purchases/${purchaseId}/charge/`); } /** * Refund a purchase * @param {string} purchaseId Purchase ID to refund * @param {number} [amountCents] Amount in cents (null for full refund) * @returns {Promise} Refund confirmation */ async refundPurchase(purchaseId, amountCents = null) { const payload = amountCents ? { amount: amountCents } : {}; return this._request('POST', `/purchases/${purchaseId}/refund/`, payload); } } // ============================================================================= // Usage Examples // ============================================================================= async function exampleCITWithInternal3DS() { const client = new PreciumS2SClient({ standardApiKey: 'your_standard_api_key', s2sApiKey: 'your_s2s_api_key', brandId: 'your_brand_id' }); try { // Step 1: Create client const customer = await client.createClient('customer@example.com'); console.log('Created client:', customer.id); // Step 2: Create purchase const purchase = await client.createPurchase({ clientId: customer.id, amountCents: 29900, currency: 'ZAR', productName: 'Premium Subscription', successRedirect: 'https://yoursite.com/success', failureRedirect: 'https://yoursite.com/failure', forceRecurring: true }); console.log('Created purchase:', purchase.id); // Step 3: Submit card data const cardData = { cardholderName: 'John Smith', cardNumber: '4000000000001091', expires: '12/28', cvc: '123', remoteIp: '196.21.45.123' }; const fingerprint = { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', acceptHeader: 'text/html,application/xhtml+xml', language: 'en-ZA', javaEnabled: false, javascriptEnabled: true, colorDepth: 24, utcOffset: -120, screenWidth: 1920, screenHeight: 1080 }; const submitResult = await client.submitCardData( purchase.direct_post_url, cardData, fingerprint, true ); // Step 4: Handle 3DS if required if (submitResult['3ds']) { console.log('3DS required - redirect customer to:', submitResult['3ds']['3ds_url']); // In production: redirect customer to 3DS URL } // Step 5: Charge after 3DS const chargeResult = await client.chargePurchase(purchase.id); console.log('Charge status:', chargeResult.status); if (chargeResult.status === 'paid') { console.log('Token:', chargeResult.payment_method_details.card.token); } } catch (error) { console.error('Error:', error.response?.data || error.message); } } module.exports = { PreciumS2SClient, ENVIRONMENTS }; ``` **Complete Integration Class** ```php 'https://gate.reviopay.com/api/v1', 'PRODUCTION' => 'https://gate.reviopay.com/api/v1' ]; private string $standardApiKey; private string $s2sApiKey; private string $brandId; private string $baseUrl; public function __construct( string $standardApiKey, string $s2sApiKey, string $brandId, string $environment = 'SANDBOX' ) { $this->standardApiKey = $standardApiKey; $this->s2sApiKey = $s2sApiKey; $this->brandId = $brandId; $this->baseUrl = self::ENVIRONMENTS[$environment]; } /** * Make an API request */ private function request( string $method, string $endpoint, ?array $data = null, bool $useS2S = false ): array { $apiKey = $useS2S ? $this->s2sApiKey : $this->standardApiKey; $url = $this->baseUrl . $endpoint; $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$apiKey}", 'Content-Type: application/json', 'Accept: application/json' ] ]); if ($data !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode >= 400) { throw new Exception("API Error: HTTP {$httpCode} - {$response}"); } return json_decode($response, true); } // ========================================================================= // Client Management // ========================================================================= /** * Create a new client */ public function createClient(string $email, ?string $phone = null): array { $payload = ['email' => $email]; if ($phone !== null) { $payload['phone'] = $phone; } return $this->request('POST', '/clients/', $payload); } /** * Get client by ID */ public function getClient(string $clientId): array { return $this->request('GET', "/clients/{$clientId}/"); } // ========================================================================= // Purchase Management // ========================================================================= /** * Create a new purchase */ public function createPurchase( string $clientId, int $amountCents, string $currency, string $productName, string $successRedirect, string $failureRedirect, bool $forceRecurring = false, ?array $metadata = null ): array { $payload = [ 'client_id' => $clientId, 'brand_id' => $this->brandId, 'currency' => $currency, 'products' => [['name' => $productName, 'price' => $amountCents]], 'success_redirect' => $successRedirect, 'failure_redirect' => $failureRedirect ]; if ($forceRecurring) { $payload['force_recurring'] = true; } if ($metadata !== null) { $payload['metadata'] = $metadata; } return $this->request('POST', '/purchases/', $payload); } /** * Create MIT (recurring) purchase */ public function createMITPurchase( string $clientId, int $amountCents, string $currency, string $productName, string $cardToken, string $networkTransactionId, int $originalAmountCents ): array { $payload = [ 'client_id' => $clientId, 'brand_id' => $this->brandId, 'currency' => $currency, 'products' => [['name' => $productName, 'price' => $amountCents]], 'payment_method_details' => [ 'card_token' => $cardToken, 'previous_network_transaction_id' => $networkTransactionId, 'original_amount_cents' => $originalAmountCents ], 'is_recurring' => true ]; return $this->request('POST', '/purchases/', $payload); } // ========================================================================= // Card Data Submission // ========================================================================= /** * Submit card data to direct post URL */ public function submitCardData( string $directPostUrl, array $cardData, array $fingerprint, bool $rememberCard = false ): array { $postData = http_build_query([ 'cardholder_name' => $cardData['cardholder_name'], 'card_number' => $cardData['card_number'], 'expires' => $cardData['expires'], 'cvc' => $cardData['cvc'], 'remember_card' => $rememberCard ? 'true' : 'false', 'remote_ip' => $cardData['remote_ip'], 'user_agent' => $fingerprint['user_agent'], 'accept_header' => $fingerprint['accept_header'], 'language' => $fingerprint['language'], 'java_enabled' => $fingerprint['java_enabled'] ? 'true' : 'false', 'javascript_enabled' => $fingerprint['javascript_enabled'] ? 'true' : 'false', 'color_depth' => (string)$fingerprint['color_depth'], 'utc_offset' => (string)$fingerprint['utc_offset'], 'screen_width' => (string)$fingerprint['screen_width'], 'screen_height' => (string)$fingerprint['screen_height'] ]); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $directPostUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$this->s2sApiKey}", 'Content-Type: application/x-www-form-urlencoded' ] ]); $response = curl_exec($ch); curl_close($ch); return json_decode($response, true); } /** * Submit token for MIT */ public function submitTokenForMIT( string $directPostUrl, string $cardToken, string $serverIp ): array { $postData = http_build_query([ 'card_token' => $cardToken, 'remote_ip' => $serverIp ]); $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $directPostUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $postData, CURLOPT_HTTPHEADER => [ "Authorization: Bearer {$this->s2sApiKey}", 'Content-Type: application/x-www-form-urlencoded' ] ]); $response = curl_exec($ch); curl_close($ch); return json_decode($response, true); } // ========================================================================= // Charge and Refund // ========================================================================= /** * Charge a purchase */ public function chargePurchase(string $purchaseId): array { return $this->request('POST', "/purchases/{$purchaseId}/charge/"); } /** * Refund a purchase */ public function refundPurchase(string $purchaseId, ?int $amountCents = null): array { $payload = $amountCents !== null ? ['amount' => $amountCents] : []; return $this->request('POST', "/purchases/{$purchaseId}/refund/", $payload); } } // ============================================================================= // Usage Example // ============================================================================= /* $client = new PreciumS2SClient( 'your_standard_api_key', 'your_s2s_api_key', 'your_brand_id' ); // Create client $customer = $client->createClient('customer@example.com'); // Create purchase $purchase = $client->createPurchase( $customer['id'], 29900, 'ZAR', 'Premium Subscription', 'https://yoursite.com/success', 'https://yoursite.com/failure', true ); // Submit card data $cardData = [ 'cardholder_name' => 'John Smith', 'card_number' => '4000000000001091', 'expires' => '12/28', 'cvc' => '123', 'remote_ip' => '196.21.45.123' ]; $fingerprint = [ 'user_agent' => 'Mozilla/5.0', 'accept_header' => 'text/html', 'language' => 'en-ZA', 'java_enabled' => false, 'javascript_enabled' => true, 'color_depth' => 24, 'utc_offset' => -120, 'screen_width' => 1920, 'screen_height' => 1080 ]; $result = $client->submitCardData( $purchase['direct_post_url'], $cardData, $fingerprint, true ); // Charge $chargeResult = $client->chargePurchase($purchase['id']); */ ``` ```ruby # Precium S2S Integration - Ruby # Requires: httparty gem require 'httparty' require 'json' class PreciumS2SClient ENVIRONMENTS = { sandbox: 'https://gate.reviopay.com/api/v1', production: 'https://gate.reviopay.com/api/v1' }.freeze def initialize(standard_api_key:, s2s_api_key:, brand_id:, environment: :sandbox) @standard_api_key = standard_api_key @s2s_api_key = s2s_api_key @brand_id = brand_id @base_url = ENVIRONMENTS[environment] end # Client Management def create_client(email:, phone: nil) payload = { email: email } payload[:phone] = phone if phone request(:post, '/clients/', payload) end def get_client(client_id) request(:get, "/clients/#{client_id}/") end # Purchase Management def create_purchase(client_id:, amount_cents:, currency:, product_name:, success_redirect:, failure_redirect:, force_recurring: false) payload = { client_id: client_id, brand_id: @brand_id, currency: currency, products: [{ name: product_name, price: amount_cents }], success_redirect: success_redirect, failure_redirect: failure_redirect } payload[:force_recurring] = true if force_recurring request(:post, '/purchases/', payload) end def create_mit_purchase(client_id:, amount_cents:, currency:, product_name:, card_token:, network_transaction_id:, original_amount_cents:) payload = { client_id: client_id, brand_id: @brand_id, currency: currency, products: [{ name: product_name, price: amount_cents }], payment_method_details: { card_token: card_token, previous_network_transaction_id: network_transaction_id, original_amount_cents: original_amount_cents }, is_recurring: true } request(:post, '/purchases/', payload) end # Card Data Submission def submit_card_data(direct_post_url:, card_data:, fingerprint:, remember_card: false) form_data = { cardholder_name: card_data[:cardholder_name], card_number: card_data[:card_number], expires: card_data[:expires], cvc: card_data[:cvc], remember_card: remember_card.to_s, remote_ip: card_data[:remote_ip], user_agent: fingerprint[:user_agent], accept_header: fingerprint[:accept_header], language: fingerprint[:language], java_enabled: fingerprint[:java_enabled].to_s, javascript_enabled: fingerprint[:javascript_enabled].to_s, color_depth: fingerprint[:color_depth].to_s, utc_offset: fingerprint[:utc_offset].to_s, screen_width: fingerprint[:screen_width].to_s, screen_height: fingerprint[:screen_height].to_s } response = HTTParty.post( direct_post_url, headers: { 'Authorization' => "Bearer #{@s2s_api_key}", 'Content-Type' => 'application/x-www-form-urlencoded' }, body: URI.encode_www_form(form_data) ) JSON.parse(response.body) end # Charge and Refund def charge_purchase(purchase_id) request(:post, "/purchases/#{purchase_id}/charge/") end def refund_purchase(purchase_id, amount_cents: nil) payload = amount_cents ? { amount: amount_cents } : {} request(:post, "/purchases/#{purchase_id}/refund/", payload) end private def request(method, endpoint, payload = nil, use_s2s: false) api_key = use_s2s ? @s2s_api_key : @standard_api_key url = "#{@base_url}#{endpoint}" options = { headers: { 'Authorization' => "Bearer #{api_key}", 'Content-Type' => 'application/json', 'Accept' => 'application/json' } } options[:body] = payload.to_json if payload response = HTTParty.send(method, url, options) JSON.parse(response.body) end end ``` ```java /** * Precium S2S Integration - Java * Requires: java.net.http (Java 11+), org.json */ package com.precium.s2s; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.HashMap; import org.json.JSONObject; public class PreciumS2SClient { private static final String BASE_URL = "https://gate.reviopay.com/api/v1"; private final String standardApiKey; private final String s2sApiKey; private final String brandId; private final HttpClient httpClient; public PreciumS2SClient(String standardApiKey, String s2sApiKey, String brandId) { this.standardApiKey = standardApiKey; this.s2sApiKey = s2sApiKey; this.brandId = brandId; this.httpClient = HttpClient.newHttpClient(); } // ========================================================================= // Client Management // ========================================================================= public JSONObject createClient(String email) throws Exception { JSONObject payload = new JSONObject(); payload.put("email", email); return request("POST", "/clients/", payload, false); } // ========================================================================= // Purchase Management // ========================================================================= public JSONObject createPurchase( String clientId, int amountCents, String currency, String productName, String successRedirect, String failureRedirect, boolean forceRecurring ) throws Exception { JSONObject product = new JSONObject(); product.put("name", productName); product.put("price", amountCents); JSONObject payload = new JSONObject(); payload.put("client_id", clientId); payload.put("brand_id", brandId); payload.put("currency", currency); payload.put("products", new org.json.JSONArray().put(product)); payload.put("success_redirect", successRedirect); payload.put("failure_redirect", failureRedirect); if (forceRecurring) { payload.put("force_recurring", true); } return request("POST", "/purchases/", payload, false); } // ========================================================================= // Card Data Submission // ========================================================================= public JSONObject submitCardData( String directPostUrl, Map cardData, Map fingerprint, boolean rememberCard ) throws Exception { Map formData = new HashMap<>(); formData.put("cardholder_name", cardData.get("cardholderName")); formData.put("card_number", cardData.get("cardNumber")); formData.put("expires", cardData.get("expires")); formData.put("cvc", cardData.get("cvc")); formData.put("remember_card", String.valueOf(rememberCard)); formData.put("remote_ip", cardData.get("remoteIp")); formData.put("user_agent", (String) fingerprint.get("userAgent")); formData.put("accept_header", (String) fingerprint.get("acceptHeader")); formData.put("language", (String) fingerprint.get("language")); formData.put("java_enabled", String.valueOf(fingerprint.get("javaEnabled"))); formData.put("javascript_enabled", String.valueOf(fingerprint.get("javascriptEnabled"))); formData.put("color_depth", String.valueOf(fingerprint.get("colorDepth"))); formData.put("utc_offset", String.valueOf(fingerprint.get("utcOffset"))); formData.put("screen_width", String.valueOf(fingerprint.get("screenWidth"))); formData.put("screen_height", String.valueOf(fingerprint.get("screenHeight"))); String formBody = buildFormData(formData); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(directPostUrl)) .header("Authorization", "Bearer " + s2sApiKey) .header("Content-Type", "application/x-www-form-urlencoded") .POST(HttpRequest.BodyPublishers.ofString(formBody)) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); return new JSONObject(response.body()); } // ========================================================================= // Charge and Refund // ========================================================================= public JSONObject chargePurchase(String purchaseId) throws Exception { return request("POST", "/purchases/" + purchaseId + "/charge/", null, false); } public JSONObject refundPurchase(String purchaseId, Integer amountCents) throws Exception { JSONObject payload = new JSONObject(); if (amountCents != null) { payload.put("amount", amountCents); } return request("POST", "/purchases/" + purchaseId + "/refund/", payload, false); } // ========================================================================= // Private Helpers // ========================================================================= private JSONObject request(String method, String endpoint, JSONObject payload, boolean useS2S) throws Exception { String apiKey = useS2S ? s2sApiKey : standardApiKey; String url = BASE_URL + endpoint; HttpRequest.Builder builder = HttpRequest.newBuilder() .uri(URI.create(url)) .header("Authorization", "Bearer " + apiKey) .header("Content-Type", "application/json") .header("Accept", "application/json"); if (method.equals("POST")) { String body = payload != null ? payload.toString() : ""; builder.POST(HttpRequest.BodyPublishers.ofString(body)); } else { builder.GET(); } HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); return new JSONObject(response.body()); } private String buildFormData(Map data) { StringBuilder sb = new StringBuilder(); for (Map.Entry entry : data.entrySet()) { if (sb.length() > 0) sb.append("&"); sb.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8)); sb.append("="); sb.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); } return sb.toString(); } } ``` ```csharp /** * Precium S2S Integration - C# * Requires: .NET 6+ */ using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; namespace Precium.S2S { public class PreciumS2SClient { private const string BaseUrl = "https://gate.reviopay.com/api/v1"; private readonly string _standardApiKey; private readonly string _s2sApiKey; private readonly string _brandId; private readonly HttpClient _httpClient; public PreciumS2SClient(string standardApiKey, string s2sApiKey, string brandId) { _standardApiKey = standardApiKey; _s2sApiKey = s2sApiKey; _brandId = brandId; _httpClient = new HttpClient(); } // Client Management public async Task CreateClientAsync(string email, string? phone = null) { var payload = new Dictionary { { "email", email } }; if (phone != null) payload["phone"] = phone; return await RequestAsync("POST", "/clients/", payload); } // Purchase Management public async Task CreatePurchaseAsync( string clientId, int amountCents, string currency, string productName, string successRedirect, string failureRedirect, bool forceRecurring = false) { var payload = new Dictionary { { "client_id", clientId }, { "brand_id", _brandId }, { "currency", currency }, { "products", new[] { new { name = productName, price = amountCents } } }, { "success_redirect", successRedirect }, { "failure_redirect", failureRedirect } }; if (forceRecurring) payload["force_recurring"] = true; return await RequestAsync("POST", "/purchases/", payload); } // Card Data Submission public async Task SubmitCardDataAsync( string directPostUrl, CardData card, BrowserFingerprint fingerprint, bool rememberCard = false) { var formData = new Dictionary { { "cardholder_name", card.CardholderName }, { "card_number", card.CardNumber }, { "expires", card.Expires }, { "cvc", card.Cvc }, { "remember_card", rememberCard.ToString().ToLower() }, { "remote_ip", card.RemoteIp }, { "user_agent", fingerprint.UserAgent }, { "accept_header", fingerprint.AcceptHeader }, { "language", fingerprint.Language }, { "java_enabled", fingerprint.JavaEnabled.ToString().ToLower() }, { "javascript_enabled", fingerprint.JavascriptEnabled.ToString().ToLower() }, { "color_depth", fingerprint.ColorDepth.ToString() }, { "utc_offset", fingerprint.UtcOffset.ToString() }, { "screen_width", fingerprint.ScreenWidth.ToString() }, { "screen_height", fingerprint.ScreenHeight.ToString() } }; var request = new HttpRequestMessage(HttpMethod.Post, directPostUrl) { Content = new FormUrlEncodedContent(formData) }; request.Headers.Add("Authorization", $"Bearer {_s2sApiKey}"); var response = await _httpClient.SendAsync(request); var content = await response.Content.ReadAsStringAsync(); return JsonDocument.Parse(content); } // Charge and Refund public async Task ChargePurchaseAsync(string purchaseId) { return await RequestAsync("POST", $"/purchases/{purchaseId}/charge/", null); } public async Task RefundPurchaseAsync(string purchaseId, int? amountCents = null) { var payload = amountCents.HasValue ? new Dictionary { { "amount", amountCents.Value } } : null; return await RequestAsync("POST", $"/purchases/{purchaseId}/refund/", payload); } private async Task RequestAsync( string method, string endpoint, Dictionary? payload, bool useS2S = false) { var apiKey = useS2S ? _s2sApiKey : _standardApiKey; var url = $"{BaseUrl}{endpoint}"; var request = new HttpRequestMessage( method == "POST" ? HttpMethod.Post : HttpMethod.Get, url); request.Headers.Add("Authorization", $"Bearer {apiKey}"); request.Headers.Add("Accept", "application/json"); if (payload != null) { var json = JsonSerializer.Serialize(payload); request.Content = new StringContent(json, Encoding.UTF8, "application/json"); } var response = await _httpClient.SendAsync(request); var content = await response.Content.ReadAsStringAsync(); return JsonDocument.Parse(content); } } public record CardData( string CardholderName, string CardNumber, string Expires, string Cvc, string RemoteIp); public record BrowserFingerprint( string UserAgent, string AcceptHeader, string Language, bool JavaEnabled, bool JavascriptEnabled, int ColorDepth, int UtcOffset, int ScreenWidth, int ScreenHeight); } ``` ```go // Precium S2S Integration - Go // Requires: Go 1.18+ package precium import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" ) const baseURL = "https://gate.reviopay.com/api/v1" type Client struct { StandardAPIKey string S2SAPIKey string BrandID string HTTPClient *http.Client } type CardData struct { CardholderName string CardNumber string Expires string CVC string RemoteIP string } type BrowserFingerprint struct { UserAgent string AcceptHeader string Language string JavaEnabled bool JavascriptEnabled bool ColorDepth int UTCOffset int ScreenWidth int ScreenHeight int } func NewClient(standardAPIKey, s2sAPIKey, brandID string) *Client { return &Client{ StandardAPIKey: standardAPIKey, S2SAPIKey: s2sAPIKey, BrandID: brandID, HTTPClient: &http.Client{}, } } // CreateClient creates a new client record func (c *Client) CreateClient(email string, phone *string) (map[string]interface{}, error) { payload := map[string]interface{}{"email": email} if phone != nil { payload["phone"] = *phone } return c.request("POST", "/clients/", payload, false) } // CreatePurchase creates a new purchase func (c *Client) CreatePurchase( clientID string, amountCents int, currency string, productName string, successRedirect string, failureRedirect string, forceRecurring bool, ) (map[string]interface{}, error) { payload := map[string]interface{}{ "client_id": clientID, "brand_id": c.BrandID, "currency": currency, "products": []map[string]interface{}{{"name": productName, "price": amountCents}}, "success_redirect": successRedirect, "failure_redirect": failureRedirect, } if forceRecurring { payload["force_recurring"] = true } return c.request("POST", "/purchases/", payload, false) } // SubmitCardData submits card data to the direct post URL func (c *Client) SubmitCardData( directPostURL string, card CardData, fingerprint BrowserFingerprint, rememberCard bool, ) (map[string]interface{}, error) { form := url.Values{} form.Set("cardholder_name", card.CardholderName) form.Set("card_number", card.CardNumber) form.Set("expires", card.Expires) form.Set("cvc", card.CVC) form.Set("remember_card", strconv.FormatBool(rememberCard)) form.Set("remote_ip", card.RemoteIP) form.Set("user_agent", fingerprint.UserAgent) form.Set("accept_header", fingerprint.AcceptHeader) form.Set("language", fingerprint.Language) form.Set("java_enabled", strconv.FormatBool(fingerprint.JavaEnabled)) form.Set("javascript_enabled", strconv.FormatBool(fingerprint.JavascriptEnabled)) form.Set("color_depth", strconv.Itoa(fingerprint.ColorDepth)) form.Set("utc_offset", strconv.Itoa(fingerprint.UTCOffset)) form.Set("screen_width", strconv.Itoa(fingerprint.ScreenWidth)) form.Set("screen_height", strconv.Itoa(fingerprint.ScreenHeight)) req, err := http.NewRequest("POST", directPostURL, strings.NewReader(form.Encode())) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+c.S2SAPIKey) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, err } return result, nil } // ChargePurchase charges a purchase func (c *Client) ChargePurchase(purchaseID string) (map[string]interface{}, error) { return c.request("POST", fmt.Sprintf("/purchases/%s/charge/", purchaseID), nil, false) } // RefundPurchase refunds a purchase func (c *Client) RefundPurchase(purchaseID string, amountCents *int) (map[string]interface{}, error) { var payload map[string]interface{} if amountCents != nil { payload = map[string]interface{}{"amount": *amountCents} } return c.request("POST", fmt.Sprintf("/purchases/%s/refund/", purchaseID), payload, false) } func (c *Client) request(method, endpoint string, payload map[string]interface{}, useS2S bool) (map[string]interface{}, error) { apiKey := c.StandardAPIKey if useS2S { apiKey = c.S2SAPIKey } url := baseURL + endpoint var body io.Reader if payload != nil { jsonBytes, err := json.Marshal(payload) if err != nil { return nil, err } body = bytes.NewReader(jsonBytes) } req, err := http.NewRequest(method, url, body) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var result map[string]interface{} if err := json.Unmarshal(respBody, &result); err != nil { return nil, err } return result, nil } ``` ## Browser Fingerprint Collection (JavaScript) [#browser-fingerprint-collection-javascript] Use this code on your frontend to collect browser fingerprint data: ```jsx function collectBrowserFingerprint() { return { userAgent: navigator.userAgent, acceptHeader: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', language: navigator.language || navigator.userLanguage, javaEnabled: navigator.javaEnabled ? navigator.javaEnabled() : false, javascriptEnabled: true, colorDepth: screen.colorDepth, utcOffset: new Date().getTimezoneOffset(), screenWidth: screen.width, screenHeight: screen.height }; } // Usage const fingerprint = collectBrowserFingerprint(); // Send fingerprint to your backend with the payment request ``` ## 3DS Challenge Form (HTML/JavaScript) [#3ds-challenge-form-htmljavascript] ```html Bank Verification
Redirecting to your bank for verification...
``` # 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 |