Table of contents

Integration guide

Introduction

Precium's Server-to-Server (S2S) integration provides sophisticated merchants with direct control over the entire payment flow. This integration pattern is designed for organizations that require complete customization of the payment experience while maintaining the highest security standards.

This documentation is intended for: Payment processors, enterprise merchants, and technical integrators who require direct API access to card data handling.

Who Should Use S2S Integration?

The S2S integration is ideal for:

  • Payment service providers (PSPs) and payment facilitators
  • Enterprise merchants with existing PCI-compliant infrastructure
  • Organizations requiring custom checkout experiences
  • Platforms integrating payments into native mobile applications
  • Businesses with specific regulatory or compliance requirements

PCI Compliance Requirements

Important: S2S integration requires your organization to maintain PCI DSS Level 1 compliance. This includes:

|Requirement|Description| |---|---| |**SAQ-D**|Annual Self-Assessment Questionnaire Type D completion| |**AoC**|Attestation of Compliance submitted annually| |**RoC**|Report on Compliance from a Qualified Security Assessor (QSA)| |**Quarterly Scans**|ASV (Approved Scanning Vendor) vulnerability scans| |**Penetration Testing**|Annual penetration testing of cardholder data environment|

If your organization does not currently hold SAQ-D certification, please contact our sales team to discuss our redirect-based integration options that reduce your PCI scope.

Quick Start Checklist

Before beginning your integration, ensure you have:

  • API Credentials: Obtain your Standard API Key and S2S API Key from the Developers section
  • Brand ID: Retrieve your unique Brand ID from your merchant dashboard
  • PCI Documentation: Confirm your SAQ-D AoC is current and on file with Precium
  • Test Environment: Access to sandbox environment credentials
  • Webhook Endpoint: Server endpoint configured to receive payment notifications
  • 3DS Implementation: Plan for 3D Secure challenge handling (required for CIT transactions)

API Base URL

All API requests are made to:

JSON

https://gate.reviopay.com/api/v1/

Authentication

Precium uses Bearer token authentication. Include your API key in the Authorisation header of every request:

JSON

Authorisation: Bearer <your_api_key>

Token Types:

  • Standard API Key: Used for creating clients and purchases
  • S2S API Key: Used for direct card data submission to the Direct Post URL

Integration Scenarios Overview

Precium S2S supports the following integration scenarios:

|Scenario|3DS|CVV Required|Token|Use Case| |---|---|---|---|---| |**Zero Authorisation (Card Validation)**|Yes|Yes|Optional|Validate card without charging| |**Pre-Authorisation**|Yes|Yes|Optional|Reserve funds for later capture| |**Pre-Authorisation without CVV**|External|No|Yes|Reserve with stored token| |**3DS Execution (Internal)**|Precium-managed|Yes|Optional|Standard CIT with Precium 3DS| |**External MPI**|Merchant-managed|Optional|Optional|Use your own 3DS provider| |**Frictionless (Clear PAN)**|None|Optional|No|Pre-approved non-3DS processing| |**MIT Recurring**|Not required|No|Yes|Subscription/recurring billing|

Scenario Selection Decision Tree

flowchart TD
    A[New Transaction] --> B{Customer Present?}
    B -->|Yes - CIT| C{Transaction Type?}
    B -->|No - MIT| D[Use MIT Recurring Flow]
    C -->|Card Validation| E[Zero Authorisation]
    C -->|Reserve Funds| F{Have CVV?}
    C -->|Immediate Charge| G{Have own 3DS?}
    F -->|Yes| H[Pre-Authorisation]
    F -->|No| I[Pre-Auth without CVV]
    G -->|No| J{Tokenize?}
    G -->|Yes| K[External MPI Flow]
    J -->|Yes| L[Internal 3DS + Token]
    J -->|No| M{Pre-approved?}
    M -->|Yes| N[Frictionless Flow]
    M -->|No| L

Zero Authorisation (Card Validation)

Zero authorisation validates a card without charging any amount. Use this to verify card details before storing them for future use.

When to Use This Scenario

  • Validating card details during customer registration
  • Verifying card before adding to wallet/stored payment methods
  • Account verification without financial transaction

Implementation

Set the purchase amount to 0 (zero cents):

Request:

JSON

POST /api/v1/purchases/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "client_id": "d9f8a7b6-c5e4-3d2a-1b0c-9e8d7f6a5b4c",
 "brand_id": "409eb80e-3782-4b1d-afa8-b779759266a5",
 "currency": "ZAR",
 "products": [
   {
     "name": "Card Validation",
     "price": 0
   }
 ],
 "success_redirect": "https://yoursite.com/validation/success",
 "failure_redirect": "https://yoursite.com/validation/failure",
 "force_recurring": true
}

Notes:

  • 3DS is still required for zero-amount authorisations
  • Set force_recurring: true if you want to tokenize the card for future use
  • The card network will validate the card details without any hold on funds

Response

The response follows the same pattern as a standard authorisation. A successful zero-auth confirms:

  • Card number is valid
  • Card is not expired
  • Card passes basic fraud checks

Pre-Authorisation (Auth Only)

Pre-authorisation reserves funds on a cardholder's account without immediate capture. Use this for delayed fulfillment scenarios.

When to Use This Scenario

  • Hotel reservations (charge at checkout)
  • Car rentals (final amount determined at return)
  • E-commerce with delayed shipping
  • Service deposits

Step 1: Create Client

JSON

POST /api/v1/clients/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "email": "customer@example.com",
 "phone": "+27123456789",
 "full_name": "Customer Name",
 "street_address": "1 Long Street",
 "city": "Cape Town",
 "state": "Western Cape",
 "zip_code": "8001",
 "country": "South Africa"
}

Step 2: Create Purchase (Auth-Only)

Create a purchase with the estimated amount. The Authorisation will place a hold on the customer's funds.

JSON

POST /api/v1/purchases/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "client_id": "{{client_id}}",
 "payment_method_whitelist": ["visa", "mastercard", "maestro"],
 "purchase": {
   "currency": "ZAR",
   "language": "en",
   "products": [
     {
       "name": "Hotel Reservation - 3 Nights",
       "price": 450000
     }
   ]
 },
 "brand_id": "{{brand_id}}",
 "reference": "BOOKING-12345",
 "send_receipt": false,
 "force_recurring": true,
 "success_redirect": "https://yoursite.com/booking/success",
 "failure_redirect": "https://yoursite.com/booking/failure"
}

Step 3: Submit Card Data (Authorisation)

Submit card details to the direct_post_url with browser fingerprint for 3DS:

JSON

POST {{direct_post_url}}?s2s=true
Authorisation: Bearer <s2s_api_key>
Content-Type: application/json

{
 "cardholder_name": "John Smith",
 "card_number": "4000000000001091",
 "expires": "12/28",
 "cvc": "123",
 "remember_card": "on",
 "remote_ip": "196.21.45.123",
 "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
 "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
}

Step 4: Handle 3DS (if required)

If the response contains 3ds parameters, redirect customer to complete authentication.

Step 5: Capture (Full or Partial)

When ready to complete the transaction, capture the authorized amount:

Full Capture:

JSON

POST /api/v1/purchases/{{purchase_id}}/capture/
Authorisation: Bearer <standard_api_key>

Partial Capture:

JSON

POST /api/v1/purchases/{{purchase_id}}/capture/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "amount": 400000
}

The amount field specifies the capture amount in cents. You can capture less than or equal to the authorized amount.

Pre-Authorisation Void

To release the hold without capturing (e.g., customer cancels):

JSON

POST /api/v1/purchases/{{purchase_id}}/cancel/
Authorisation: Bearer <standard_api_key>

Important: Pre-Authorisations typically expire after 7-30 days depending on the card network. Capture before expiration to avoid declined transactions.

Pre-Authorisation without CVV

Use this scenario when you have a stored token and need to pre-authorize without the CVV.

When to Use This Scenario

  • Subsequent Authorisations using stored card token
  • Customer not present but has previously authorized card storage
  • Recurring pre-Authorisations (e.g., monthly car rental reservations)

Implementation

Create purchase with token reference and recurring flags:

JSON

POST /api/v1/purchases/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "client_id": "{{client_id}}",
 "payment_method_whitelist": ["visa", "mastercard", "maestro"],
 "purchase": {
   "currency": "ZAR",
   "language": "en",
   "products": [
     {
       "name": "Recurring Pre-Authorisation",
       "price": 500000
     }
   ],
   "payment_method_details": {
     "card": {
       "is_recurring": true,
       "previous_network_transaction_id": "546464487987132",
       "original_amount_cents": "10000"
     }
   }
 },
 "brand_id": "{{brand_id}}",
 "reference": "PREAUTH-TOKEN-001",
 "send_receipt": false,
 "force_recurring": false
}

Submit to direct_post_url with token (no CVV required):

JSON

POST {{direct_post_url}}?s2s=true
Authorisation: Bearer <s2s_api_key>
Content-Type: application/json

{
 "cardholder_name": "John Smith",
 "card_number": "4000000000001091",
 "expires": "12/28",
 "cvc": "000",
 "remember_card": "off",
 "remote_ip": "8.8.8.8"
}

Note: When using stored credentials, CVV can be set to "000" or any placeholder value.

3DS Execution (Internal - Precium Managed)

This is the standard flow for customer-initiated transactions where Precium handles 3D Secure authentication.

Sequence Diagram

sequenceDiagram
    participant Customer
    participant Merchant
    participant Precium
    participant CardNetwork as Card Network/Issuer

    Note over Merchant,Precium: Step 1: Create Client
    Merchant->>Precium: POST /clients/
    Precium-->>Merchant: Client ID

    Note over Merchant,Precium: Step 2: Create Purchase
    Merchant->>Precium: POST /purchases/
    Precium-->>Merchant: Purchase Object with direct_post_url

    Note over Merchant,Precium: Step 3: Submit Card Data
    Merchant->>Precium: POST to direct_post_url (card details + browser fingerprint)
    Precium-->>Merchant: 3DS Parameters (MD, PaReq, 3DS URL)

    Note over Customer,CardNetwork: Step 4: 3DS Challenge
    Merchant->>Customer: Redirect to 3DS URL
    Customer->>CardNetwork: Complete Authentication
    CardNetwork->>Precium: 3DS Result (callback_url)

    Note over Merchant,Precium: Step 5: Capture Transaction
    Merchant->>Precium: POST /purchases/{id}/capture/ (via callback)
    Precium->>CardNetwork: Capture Request
    CardNetwork-->>Precium: Capture Response
    Precium-->>Merchant: Transaction Result + Token

Step-by-Step Implementation

Step 1: Create Client

JSON

POST /api/v1/clients/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "email": "customer@example.com",
 "phone": "+27123456789",
 "full_name": "Customer Name",
 "street_address": "1 Long Street",
 "city": "Cape Town",
 "state": "Western Cape",
 "zip_code": "8001",
 "country": "South Africa"
}

Step 2: Create Purchase

JSON

POST /api/v1/purchases/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "client_id": "{{client_id}}",
 "payment_method_whitelist": ["visa", "mastercard", "maestro"],
 "purchase": {
   "currency": "ZAR",
   "language": "en",
   "products": [
     {
       "name": "Premium Subscription",
       "price": 29900
     }
   ]
 },
 "brand_id": "{{brand_id}}",
 "reference": "ORDER-12345",
 "send_receipt": false,
 "force_recurring": true,
 "success_redirect": "https://yoursite.com/payment/success",
 "failure_redirect": "https://yoursite.com/payment/failure",
 "cancel_redirect": "https://yoursite.com/payment/cancel"
}

Step 3: Submit Card Data (Authorisation)

JSON

POST {{direct_post_url}}?s2s=true
Authorisation: Bearer <s2s_api_key>
Content-Type: application/json

{
 "cardholder_name": "John Smith",
 "card_number": "4000000000001091",
 "expires": "12/28",
 "cvc": "123",
 "remember_card": "on",
 "remote_ip": "196.21.45.123",
 "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
 "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
}

Response (3DS Required):

JSON

{
 "md": "abc123xyz789...",
 "PaReq": "eJxVUt1ugjAUvvcpSO...",
 "URL": "https://acs.cardnetwork.com/3ds/challenge",
 "callback_url": "https://gate.reviopay.com/3ds/callback/a1b2c3d4..."
}

Step 4: Handle 3DS Challenge

Create an HTML form that auto-submits to the 3DS URL:

HTML

<form id="3ds-form" action="{{URL}}" method="POST">
 <input type="hidden" name="MD" value="{{md}}" />
 <input type="hidden" name="PaReq" value="{{PaReq}}" />
 <input type="hidden" name="TermUrl" value="{{callback_url}}" />
 <noscript>
   <button type="submit">Continue to Bank Verification</button>
 </noscript>
</form>
<script>document.getElementById('3ds-form').submit();</script>

Step 5: Capture (After 3DS Completion)

After the customer completes 3DS, submit the callback to capture:

JSON

POST {{callback_url}}
Content-Type: application/x-www-form-urlencoded

MD={{MD}}&PaRes={{PaRes}}

Success Response:

JSON

{
 "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
 "status": "paid",
 "transaction_id": "txn_9876543210",
 "payment_method_details": {
   "card": {
     "brand": "visa",
     "last_4": "1091",
     "exp_month": 12,
     "exp_year": 2028,
     "token": "tok_abc123def456...",
     "network_transaction_id": "MCC123456789012345"
   }
 }
}

External MPI (External 3DS Provider)

Use this flow when you have your own 3D Secure Merchant Plug-In (MPI) from providers like Cardinal Commerce, Netcetera, or similar.

When to Use This Scenario

  • You have an existing 3DS solution certified for your card networks
  • You require specific 3DS configuration or branding
  • You need to consolidate 3DS authentication across multiple acquirers

Implementation

Include 3DS authentication results in the purchase creation:

JSON

POST /api/v1/purchases/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "client_id": "{{client_id}}",
 "payment_method_whitelist": ["visa", "mastercard", "maestro"],
 "purchase": {
   "currency": "ZAR",
   "language": "en",
   "products": [
     {
       "name": "Once off charge",
       "price": 50000
     }
   ],
   "payment_method_details": {
     "card": {
       "is_external_3DS": true,
       "authentication_transaction_id": "GAux5DosxbhI35RL8nc0",
       "cavv": "AAABBBCCCdddEEEfff111222333=",
       "xid": "xidTestValue",
       "eci_raw": "05"
     }
   }
 },
 "brand_id": "{{brand_id}}",
 "reference": "EXT3DS-ORDER-001",
 "send_receipt": false,
 "force_recurring": false,
 "success_redirect": "https://yoursite.com/success",
 "failure_redirect": "https://yoursite.com/failure"
}

External 3DS Fields:

|Field|Type|Required|Description| |---|---|---|---| |`is_external_3DS`|boolean|Yes|Must be `true` for external MPI| |`authentication_transaction_id`|string|Yes|Your MPI's transaction ID| |`cavv`|string|Yes|Cardholder Authentication Verification Value| |`xid`|string|3DS1|Transaction identifier (3DS1 only)| |`eci_raw`|string|Yes|Electronic Commerce Indicator|

ECI Values Reference:

|ECI|Meaning|Liability Shift| |---|---|---| |05|Fully authenticated (Visa)|Yes| |02|Fully authenticated (Mastercard)|Yes| |06|Attempted authentication (Visa)|Conditional| |01|Attempted authentication (Mastercard)|Conditional| |07|Non-3DS or failed (Visa)|No| |00|Non-3DS or failed (Mastercard)|No|

Submit Card Data

JSON

POST {{direct_post_url}}?s2s=true
Authorisation: Bearer <s2s_api_key>
Content-Type: application/json

{
 "cardholder_name": "John Smith",
 "card_number": "4000000000001091",
 "expires": "12/28",
 "cvc": "000",
 "remember_card": "off",
 "remote_ip": "8.8.8.8",
 "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
}

Note: This must be performed on a Non-3DS brand - contact your account manager to configure.

External Network Token Provisioning

Use external network tokens (MDES/VTS) for enhanced security and improved Authorisation rates.

When to Use This Scenario

  • You provision tokens directly from Visa Token Service (VTS) or Mastercard Digital Enablement Service (MDES)
  • You want to leverage network-level tokenization benefits
  • You need to maintain token lifecycle independently

Implementation

Include the network token and cryptogram in your purchase:

JSON

POST /api/v1/purchases/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "client_id": "{{client_id}}",
 "purchase": {
   "currency": "ZAR",
   "language": "en",
   "products": [
     {
       "name": "Network Token Purchase",
       "price": 25000
     }
   ],
   "payment_method_details": {
     "card": {
       "network_token": "4895370012341234",
       "network_token_cryptogram": "AgAAAAAABk4DWZ4C28yUQAAAAAA=",
       "token_requestor_id": "50110030273",
       "is_recurring": true
     }
   }
 },
 "brand_id": "{{brand_id}}",
 "reference": "NETWORK-TOKEN-001"
}

Network Token Fields:

|Field|Type|Required|Description| |---|---|---|---| |`network_token`|string|Yes|The network token (DPAN)| |`network_token_cryptogram`|string|Yes|Dynamic cryptogram from token service| |`token_requestor_id`|string|Yes|Your registered Token Requestor ID|

Frictionless (Clear PAN Processing - No 3DS)

This flow bypasses 3D Secure authentication entirely. Pre-approval from Precium is required.

When to Use This Scenario

  • Low-value transactions within approved thresholds
  • Markets where 3DS adoption is limited
  • Specific merchant categories with pre-negotiated exemptions
  • Testing and development environments

Risk and Liability Warning

Transactions processed without 3DS authentication:

  • Do not receive liability shift protection
  • Have higher chargeback exposure
  • May have higher decline rates from issuers
  • Are subject to monitoring and may be disabled if fraud rates exceed thresholds

Note: This flow is technically not permitted in South Africa due to 3DS requirements. The first charge must always be performed with 3DS; subsequent recurring charges can be processed without 3DS.

Implementation

JSON

POST /api/v1/purchases/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "client_id": "{{client_id}}",
 "payment_method_whitelist": ["visa", "mastercard", "maestro"],
 "purchase": {
   "currency": "ZAR",
   "language": "en",
   "products": [
     {
       "name": "Frictionless charge",
       "price": 5000
     }
   ]
 },
 "brand_id": "{{brand_id}}",
 "reference": "FRICTIONLESS-001",
 "send_receipt": false,
 "force_recurring": false,
 "success_redirect": "https://yoursite.com/success",
 "failure_redirect": "https://yoursite.com/failure"
}

Submit card data without browser fingerprint:

JSON

POST {{direct_post_url}}?s2s=true
Authorisation: Bearer <s2s_api_key>
Content-Type: application/json

{
 "cardholder_name": "John Smith",
 "card_number": "4000000000001091",
 "expires": "12/28",
 "cvc": "000",
 "remember_card": "off",
 "remote_ip": "8.8.8.8"
}

The response will indicate ready for charge without 3DS parameters.

MIT Recurring (Merchant-Initiated Transactions)

MIT transactions are used for recurring charges where the cardholder is not present.

When to Use This Scenario

  • Subscription billing cycles
  • Recurring membership fees
  • Instalment payment plans
  • Variable-amount recurring charges (utilities, usage-based billing)

Prerequisites

To process MIT transactions, you must have:

  1. Stored Token: Card token from an initial CIT transaction with force_recurring: true
  2. Network Transaction ID: The network_transaction_id from the original transaction
  3. Original Amount: The original_amount_cents from the tokenization transaction
  4. Customer Consent: Documented Authorisation for recurring charges

Implementation

JSON

POST /api/v1/purchases/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "client_id": "{{client_id}}",
 "payment_method_whitelist": ["visa", "mastercard", "maestro"],
 "purchase": {
   "currency": "ZAR",
   "language": "en",
   "products": [
     {
       "name": "Monthly Subscription - February 2026",
       "price": 29900
     }
   ],
   "payment_method_details": {
     "card": {
       "is_recurring": true,
       "previous_network_transaction_id": "546464487987132",
       "original_amount_cents": "10000"
     }
   }
 },
 "brand_id": "{{brand_id}}",
 "reference": "SUB-FEB-2026",
 "send_receipt": false,
 "force_recurring": false,
 "success_redirect": "https://yoursite.com/success",
 "failure_redirect": "https://yoursite.com/failure"
}

Submit with token:

JSON

POST {{direct_post_url}}?s2s=true
Authorisation: Bearer <s2s_api_key>
Content-Type: application/json

{
 "cardholder_name": "John Smith",
 "card_number": "4000000000001091",
 "expires": "12/28",
 "cvc": "000",
 "remember_card": "off",
 "remote_ip": "8.8.8.8"
}

Charge using the recurring token:

JSON

POST /api/v1/purchases/{{purchase_id}}/charge/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "recurring_token": "{{original_purchase_id}}"
}

Partial Captures

Capture less than the authorized amount when the final charge is lower than the pre-Authorisation.

When to Use

  • Customer modifies order after Authorisation
  • Partial fulfillment scenarios
  • Final charge is less than estimated

Implementation

JSON

POST /api/v1/purchases/{{purchase_id}}/capture/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "amount": 35000
}

Notes:

  • The amount must be less than or equal to the authorized amount
  • Once captured, you cannot capture additional amounts
  • The remaining Authorisation is automatically released

Capture Void (Before Settlement)

Cancel a capture before settlement to avoid processing fees and immediate fund release.

When to Use

  • Customer cancels order after capture but before settlement
  • Capture made in error
  • Immediate reversal needed

Implementation

JSON

POST /api/v1/purchases/{{purchase_id}}/cancel/
Authorisation: Bearer <standard_api_key>

Response:

JSON

{
 "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
 "status": "cancelled",
 "cancelled_on": "2026-01-13T14:30:00Z"
}

Notes:

  • Only available before settlement (typically same day)
  • After settlement, use refund instead
  • Void releases the hold immediately on most issuing banks

Partial Refunds

Refund a portion of a settled transaction.

Implementation

POST /api/v1/purchases/{{purchase_id}}/refund/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json

{
 "amount": 15000
}

Notes:

  • amount is in cents (15000 = R150.00)
  • Multiple partial refunds can be processed until the original total is reached
  • Each refund generates a separate refund_id

Response

JSON

{
 "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
 "status": "refunded",
 "refund_amount": 15000,
 "refunded_on": "2026-01-14T15:30:00Z",
 "refund_id": "ref_xyz789abc123"
}

Refund Void (Before Settlement)

Cancel a refund before settlement.

When to Use

  • Refund issued in error
  • Customer changes mind before refund settles
  • Immediate reversal of refund needed

Implementation

JSON

POST /api/v1/purchases/{{purchase_id}}/refund/{{refund_id}}/cancel/
Authorisation: Bearer <standard_api_key>

Notes:

  • Only available before refund settlement
  • After settlement, you would need to process a new charge (with customer consent)

Settlement Timing Reference

|Action|Before Settlement|After Settlement| |---|---|---| |Cancel Authorisation|✅ Void|❌ Not possible| |Cancel Capture|✅ Void|❌ Use refund| |Cancel Refund|✅ Void|❌ Requires new charge| |Partial Capture|✅ Supported|❌ Already settled| |Partial Refund|N/A|✅ Supported|

Settlement typically occurs:

  • Same-day for most transactions
  • Next business day for late transactions
  • Check with your account manager for specific settlement windows

/Webhooks

Webhooks provide real-time notifications of transaction events, enabling you to update your systems immediately when payments are processed, refunded, or encounter issues.

Webhook Setup

Step 1: Configure Your Endpoint

Create an HTTPS endpoint on your server to receive webhook notifications:

HTML

https://yoursite.com/webhooks/precium

Endpoint Requirements:

|Requirement|Details| |---|---| |**Protocol**|HTTPS required (HTTP only in sandbox)| |**Method**|POST| |**Response Time**|Must respond within 5 seconds| |**Response Code**|Return HTTP 200 to acknowledge receipt| |**Content-Type**|Expect `application/json`|

Step 2: Register Webhook URL

Register your webhook URL in the Precium dashboard:

  1. Navigate to Settings → Webhooks
  2. Click Add Webhook Endpoint
  3. Enter your endpoint URL
  4. Select the events you want to receive
  5. Copy and securely store your Webhook Secret (used for signature verification)

Alternatively, configure via API:

BASH

curl -X POST https://gate.reviopay.com/api/v1/webhooks/ \
 -H "Authorisation: Bearer YOUR_STANDARD_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
   "url": "https://yoursite.com/webhooks/precium",
   "events": ["purchase.paid", "purchase.payment_failure", "purchase.refunded"],
   "active": true
 }'

Webhook Authentication

Precium signs all webhook payloads to ensure authenticity. Always verify webhook signatures in production.

Signature Verification

Each webhook request includes these headers:

|Header|Description| |---|---| |`X-Webhook-Signature`|HMAC-SHA256 signature of the payload| |`X-Webhook-Timestamp`|Unix timestamp when webhook was sent| |`X-Webhook-ID`|Unique identifier for this webhook delivery|

Signature Algorithm

The signature is computed as:

HMAC-SHA256(webhook_secret, timestamp + "." + raw_payload)

Verification Example (Python)

PYTHON

import hmac
import hashlib
import time
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_from_dashboard"

def verify_webhook_signature(payload: bytes, signature: str, timestamp: str) -> bool:
   """Verify the webhook signature."""
   # Check timestamp is recent (within 5 minutes)
   current_time = int(time.time())
   webhook_time = int(timestamp)
   if abs(current_time - webhook_time) > 300:
       return False  # Replay attack protection

   # Compute expected signature
   message = f"{timestamp}.{payload.decode('utf-8')}"
   expected_signature = hmac.new(
       WEBHOOK_SECRET.encode('utf-8'),
       message.encode('utf-8'),
       hashlib.sha256
   ).hexdigest()

   # Constant-time comparison to prevent timing attacks
   return hmac.compare_digest(signature, expected_signature)

@app.route('/webhooks/precium', methods=['POST'])
def handle_webhook():
   # Get signature headers
   signature = request.headers.get('X-Webhook-Signature')
   timestamp = request.headers.get('X-Webhook-Timestamp')
   webhook_id = request.headers.get('X-Webhook-ID')

   if not signature or not timestamp:
       abort(401, "Missing signature headers")

   # Verify signature
   if not verify_webhook_signature(request.data, signature, timestamp):
       abort(401, "Invalid signature")

   # Process webhook
   event = request.json
   process_webhook_async(event, webhook_id)

   return '', 200

Verification Example (Node.js)

JSX

const crypto = require('crypto');
const express = require('express');

const app = express();
const WEBHOOK_SECRET = 'your_webhook_secret_from_dashboard';

function verifyWebhookSignature(payload, signature, timestamp) {
 // Check timestamp is recent (within 5 minutes)
 const currentTime = Math.floor(Date.now() / 1000);
 const webhookTime = parseInt(timestamp, 10);
 if (Math.abs(currentTime - webhookTime) > 300) {
   return false;
 }

 // Compute expected signature
 const message = `${timestamp}.${payload}`;
 const expectedSignature = crypto
   .createHmac('sha256', WEBHOOK_SECRET)
   .update(message)
   .digest('hex');

 // Constant-time comparison
 return crypto.timingSafeEqual(
   Buffer.from(signature),
   Buffer.from(expectedSignature)
 );
}

app.post('/webhooks/precium', express.raw({ type: 'application/json' }), (req, res) => {
 const signature = req.headers['x-webhook-signature'];
 const timestamp = req.headers['x-webhook-timestamp'];
 const webhookId = req.headers['x-webhook-id'];

 if (!signature || !timestamp) {
   return res.status(401).send('Missing signature headers');
 }

 if (!verifyWebhookSignature(req.body.toString(), signature, timestamp)) {
   return res.status(401).send('Invalid signature');
 }

 // Process webhook asynchronously
 const event = JSON.parse(req.body);
 processWebhookAsync(event, webhookId);

 res.status(200).send();
});

Verification Example (PHP)

PHP

<?php
$webhookSecret = 'your_webhook_secret_from_dashboard';

function verifyWebhookSignature($payload, $signature, $timestamp) {
   global $webhookSecret;

   // Check timestamp is recent (within 5 minutes)
   $currentTime = time();
   $webhookTime = intval($timestamp);
   if (abs($currentTime - $webhookTime) > 300) {
       return false;
   }

   // Compute expected signature
   $message = $timestamp . '.' . $payload;
   $expectedSignature = hash_hmac('sha256', $message, $webhookSecret);

   // Constant-time comparison
   return hash_equals($expectedSignature, $signature);
}

// Get headers
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$webhookId = $_SERVER['HTTP_X_WEBHOOK_ID'] ?? '';

// Get raw payload
$payload = file_get_contents('php://input');

if (!$signature || !$timestamp) {
   http_response_code(401);
   exit('Missing signature headers');
}

if (!verifyWebhookSignature($payload, $signature, $timestamp)) {
   http_response_code(401);
   exit('Invalid signature');
}

// Process webhook
$event = json_decode($payload, true);
processWebhookAsync($event, $webhookId);

http_response_code(200);

Webhook Events

|Event|Description|Triggered When| |---|---|---| |`purchase.pending`|Transaction awaiting completion|3DS challenge initiated, manual review| |`purchase.authorized`|Pre-Authorisation successful|Funds reserved, awaiting capture| |`purchase.captured`|Capture completed|Pre-auth captured| |`purchase.paid`|Payment successful|Transaction completed and settled| |`purchase.payment_failure`|Payment failed|Decline, fraud block, or error| |`purchase.cancelled`|Transaction voided|Pre-auth or capture voided| |`purchase.refunded`|Refund processed|Full or partial refund completed| |`purchase.refund_failure`|Refund failed|Refund could not be processed| |`purchase.chargeback`|Chargeback received|Customer disputed transaction|

Webhook Payload Structure

JSON

{
 "id": "wh_a1b2c3d4e5f6",
 "event": "purchase.paid",
 "timestamp": "2026-01-13T10:35:12Z",
 "api_version": "2026-01-01",
 "data": {
   "purchase_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
   "status": "paid",
   "amount": 29900,
   "currency": "ZAR",
   "client_id": "d9f8a7b6-c5e4-3d2a-1b0c-9e8d7f6a5b4c",
   "brand_id": "your_brand_id",
   "created_at": "2026-01-13T10:34:00Z",
   "completed_at": "2026-01-13T10:35:12Z",
   "payment_method": {
     "type": "card",
     "brand": "visa",
     "last_4": "1091",
     "exp_month": 12,
     "exp_year": 2028,
     "cardholder_name": "John Smith"
   },
   "metadata": {
     "order_id": "ORD-12345"
   }
 }
}

Refund Event Payload

JSON

{
 "id": "wh_f6e5d4c3b2a1",
 "event": "purchase.refunded",
 "timestamp": "2026-01-14T09:15:00Z",
 "data": {
   "purchase_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
   "status": "refunded",
   "refund": {
     "id": "ref_xyz789",
     "amount": 10000,
     "reason": "Customer request",
     "created_at": "2026-01-14T09:14:55Z"
   },
   "original_amount": 29900,
   "total_refunded": 10000,
   "remaining": 19900
 }

Authorisation Event Payload (Pre-Auth)

JSON

{
 "id": "wh_pre123auth",
 "event": "purchase.authorized",
 "timestamp": "2026-01-13T14:22:00Z",
 "data": {
   "purchase_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
   "status": "authorized",
   "amount": 500000,
   "currency": "ZAR",
   "Authorisation": {
     "expires_at": "2026-01-20T14:22:00Z",
     "network_transaction_id": "MCC123456789"
   }
 }
}

Webhook Retry Policy

If your endpoint doesn't respond with HTTP 200, Precium retries with exponential backoff:

|Attempt|Delay| |---|---| |1|Immediate| |2|1 minute| |3|5 minutes| |4|30 minutes| |5|2 hours| |6|8 hours| |7|24 hours|

After 7 failed attempts, the webhook is marked as failed and an alert is sent.

Webhook Best Practices

  1. Respond quickly: Return HTTP 200 immediately, process asynchronously
  2. Implement idempotency: Use X-Webhook-ID to detect and handle duplicates
  3. Verify signatures: Always validate in production to prevent spoofing
  4. Handle retries: Store webhook ID and skip already-processed events
  5. Log everything: Log webhook ID, event type, and processing result
  6. Monitor failures: Set up alerts for repeated webhook failures
  7. Use queues: Push webhook data to a message queue for reliable processing

Idempotency Implementation

PYTHON

# Use Redis or database to track processed webhooks
processed_webhooks = redis.Redis()

def process_webhook_async(event, webhook_id):
   # Check if already processed
   if processed_webhooks.exists(f"webhook:{webhook_id}"):
       return  # Already handled

   # Mark as processing (with expiry)
   processed_webhooks.setex(f"webhook:{webhook_id}", 86400, "processing")

   try:
       # Process the event
       handle_event(event)
       processed_webhooks.setex(f"webhook:{webhook_id}", 86400, "completed")
   except Exception as e:
       processed_webhooks.delete(f"webhook:{webhook_id}")
       raise

Testing Webhooks

Sandbox Testing

In sandbox, you can trigger test webhooks:

BASH

curl -X POST https://gate.reviopay.com/api/v1/webhooks/test/ \
 -H "Authorisation: Bearer YOUR_STANDARD_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{
   "event": "purchase.paid",
   "webhook_url": "https://yoursite.com/webhooks/precium"
 }'

Local Development

For local development, use a tunnelling service:

BASH

# Using ngrok
ngrok http 3000

# Your webhook URL becomes:
# https://abc123.ngrok.io/webhooks/precium

Webhook Security Checklist

  • Endpoint uses HTTPS
  • Signature verification implemented
  • Timestamp validation (5-minute window)
  • Idempotency handling for duplicate deliveries
  • Response within 5 seconds
  • Async processing for heavy operations
  • Logging of all webhook events
  • Alerting on repeated failures
  • Webhook secret stored securely (not in code)

Testing

Test Card Numbers

|Card Number|Behavior| |---|---| |`4000000000001091`|Successful payment with 3DS| |`4000000000001000`|Successful payment, no 3DS| |`5555555555554444`|Mastercard success| |`4000000000000002`|Card declined| |`4000000000000069`|Expired card| |`4000000000000127`|Incorrect CVC| |`4000000000000119`|Processing error|

Test CVCs

|CVC|Behavior| |---|---| |`123`|Successful verification| |`000`|Bypass CVC (for recurring/tokenized)|

Next Steps

  • Error Codes Reference - Complete error code documentation
  • Code Examples - Implementation examples in multiple languages
  • Glossary - Payment terminology definitions
  • Troubleshooting - Common issues and solutions