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:
PCI Compliance Requirements
Important: S2S integration requires your organization to maintain PCI DSS Level 1 compliance. This includes:
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.
Before beginning your integration, ensure you have:
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:
Precium S2S supports the following integration scenarios:
Scenario Selection Decision Tree
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
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:
force_recurring: true if you want to tokenize the card for future use
Response
The response follows the same pattern as a standard authorisation. A successful zero-auth confirms:
Pre-authorisation reserves funds on a cardholder's account without immediate capture. Use this for delayed fulfillment scenarios.
When to Use This Scenario
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.
Use this scenario when you have a stored token and need to pre-authorize without the CVV.
When to Use This Scenario
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.
This is the standard flow for customer-initiated transactions where Precium handles 3D Secure authentication.
Sequence Diagram
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"
}
}
}
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
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:
ECI Values Reference:
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.
Use external network tokens (MDES/VTS) for enhanced security and improved Authorisation rates.
When to Use This Scenario
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:
This flow bypasses 3D Secure authentication entirely. Pre-approval from Precium is required.
When to Use This Scenario
Risk and Liability Warning
Transactions processed without 3DS authentication:
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 transactions are used for recurring charges where the cardholder is not present.
When to Use This Scenario
Prerequisites
To process MIT transactions, you must have:
force_recurring: truenetwork_transaction_id from the original transactionoriginal_amount_cents from the tokenization transaction
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}}"
}
Capture less than the authorized amount when the final charge is lower than the pre-Authorisation.
When to Use
Implementation
JSON
POST /api/v1/purchases/{{purchase_id}}/capture/
Authorisation: Bearer <standard_api_key>
Content-Type: application/json
{
"amount": 35000
}
Notes:
amount must be less than or equal to the authorized amount
Cancel a capture before settlement to avoid processing fees and immediate fund release.
When to Use
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:
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)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"
}
Cancel a refund before settlement.
When to Use
Implementation
JSON
POST /api/v1/purchases/{{purchase_id}}/refund/{{refund_id}}/cancel/
Authorisation: Bearer <standard_api_key>
Notes:
Settlement typically occurs:
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:
Step 2: Register Webhook URL
Register your webhook URL in the Precium dashboard:
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:
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
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:
After 7 failed attempts, the webhook is marked as failed and an alert is sent.
Webhook Best Practices
X-Webhook-ID to detect and handle duplicates
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
Test Card Numbers
Test CVCs