PayVault API
Integrate secure EPS payments into your application. Accept payments, manage refunds, and receive real-time notifications.
Overview
PayVault is a self-hosted payment gateway that processes payments through EPS (Electronic Payment System). Your application creates a bill via the API, receives a payment URL, and redirects customers to a hosted payment page where they complete payment via bKash, Nagad, card, or mobile banking.
All API endpoints use the base path: /api/v1
Example: https://payvault.trialvo.com/api/v1/bills
Services can be created in sandbox mode. Sandbox transactions use the EPS test environment and don't process real payments. Use sandbox for development and testing.
Quick Start — Your First Payment in 5 Minutes
Follow these steps to create your first bill and accept a payment.
Ask your PayVault administrator to create a service for your project. You will receive:
Service ID— a UUID identifying your serviceAPI Key— a secret key for signing requests (64-character hex string)
From your backend, send a signed POST /api/v1/bills request with customer and order details. You get back a pay_url.
Redirect your customer to the pay_url. PayVault shows a hosted payment page. The customer selects a payment method (bKash, Nagad, card, etc.) and completes payment.
After payment completes (or fails), PayVault sends a POST webhook to your registered IPN endpoint with the payment result. Verify the signature and update your order.
After payment, the customer is redirected to your success_url, fail_url, or cancel_url. Do not rely on this redirect for order fulfillment — always use the IPN webhook.
Never make API calls from the frontend. Your API Key must be kept secret and used only from your backend server. The HMAC signature must be computed server-side.
Getting Your Credentials
Your PayVault admin sets up your service. Here's what you'll receive and where to find it:
1. Service ID
A UUID like f47ac10b-58cc-4372-a567-0e02b2c3d479. Found in the admin panel under Services. This is public — it identifies which service is making a request.
2. API Key
A 64-character hex string like a1b2c3d4e5f6.... Generated in admin panel under Services → Generate Key. The full key can be revealed by clicking Reveal Key on the service detail page.
Treat your API Key like a password. Store it in environment variables or a secrets manager. Never commit it to version control. If compromised, ask your admin to revoke it and generate a new one.
3. IPN Secret
When your admin registers an IPN endpoint for your service, a separate IPN secret is generated. This is used to verify that incoming webhooks are genuinely from PayVault.
Payment Flow
Here's the complete lifecycle of a payment:
Your Backend → PayVault API
Call POST /api/v1/bills with order + customer details. You receive a pay_url and bill_token.
Customer → Payment Page
Redirect customer to pay_url. PayVault displays a hosted payment page with available payment methods.
Payment Processing
Customer pays via EPS (bKash, Nagad, card, mobile banking). EPS processes the payment and sends a callback to PayVault.
PayVault → Your IPN Endpoint
PayVault updates the bill status and sends a signed POST webhook to your registered IPN endpoint with the result.
Customer → Your Website
Customer is redirected to your success_url, fail_url, or cancel_url. This is for UX only — fulfillment must be based on the IPN.
Authentication — HMAC-SHA256 Signing
Every API request must include authentication headers with an HMAC-SHA256 signature. This ensures request integrity, prevents tampering, and authenticates your service.
Required Headers
| Header | Description | Example |
|---|---|---|
Content-Type | Must be application/json | application/json |
X-Service-Id | Your service UUID | f47ac10b-58cc-... |
X-Api-Key | Your full API key (64-char hex) | a1b2c3d4e5f6... |
X-Timestamp | Current Unix timestamp (seconds) | 1750572622 |
X-Nonce | Unique random string (UUID recommended) | 550e8400-e29b-... |
X-Body-Hash | SHA-256 of request body (hex, 64 chars) | 5d41402abc4b2a... |
X-Signature | HMAC-SHA256 signature (hex, 64 chars) | f7bc83f430538... |
Step-by-Step Signature Computation
- Serialize your JSON request body as a string.
- Hash the body:
body_hash = SHA-256(body_string)→ hex string (64 chars). For GET/DELETE requests with no body, use the SHA-256 of an empty string:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. - Build the message: concatenate with colons:
{service_id}:{timestamp}:{nonce}:{body_hash} - Sign:
signature = HMAC-SHA256(api_key, message)→ hex string (64 chars). The API key is used as raw UTF-8 bytes (the string itself, not hex-decoded).
// Pseudocode
body_string = '{"customer_name":"John",...}'
body_hash = SHA256(body_string) // hex, 64 chars
message = "{service_id}:{timestamp}:{nonce}:{body_hash}"
signature = HMAC_SHA256(api_key_as_utf8_bytes, message) // hex, 64 chars
Replay Protection
PayVault enforces two layers of replay protection:
| Check | Rule | Error if violated |
|---|---|---|
| Timestamp | Request must be within 5 minutes in the past or 60 seconds in the future of server time | Request timestamp expired |
| Nonce | Each nonce can only be used once within a 10-minute window (per service) | Nonce already used (replay attack) |
Ensure your server's clock is synchronized with NTP. If your server clock drifts more than 5 minutes from UTC, all requests will be rejected.
Create Bill
Creates a new payment bill and returns a payment URL to redirect the customer to.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
customer_name | string | ✓ | Customer full name |
customer_email | string | ✓ | Customer email address |
customer_phone | string | ✓ | Customer phone number |
customer_address | string | ✓ | Billing address line 1 |
customer_city | string | ✓ | Billing city |
customer_state | string | ✓ | Billing state / division |
customer_postcode | string | ✓ | Billing postal code |
subtotal | decimal | ✓ | Subtotal before discounts/tax (e.g. "1500.00") |
final_amount | decimal | ✓ | Final amount to charge the customer |
items | array | ✓ | Array of bill items (see below) |
external_order_id | string | Your system's order ID (returned in IPN) | |
external_invoice_id | string | Your invoice reference | |
currency | string | Currency code (default: BDT) | |
payment_type | string | one_time (default) or subscription | |
total_discount | decimal | Total discount applied | |
tax_amount | decimal | Tax amount | |
shipping_amount | decimal | Shipping cost | |
success_url | string | Redirect URL on payment success | |
fail_url | string | Redirect URL on payment failure | |
cancel_url | string | Redirect URL if customer cancels | |
meta | object | Custom metadata — stored and returned in IPN |
Bill Item Fields
| Field | Type | Required | Description |
|---|---|---|---|
product_name | string | ✓ | Name of the product |
quantity | integer | ✓ | Quantity purchased |
unit_selling_price | decimal | ✓ | Price per unit before discount |
unit_final_price | decimal | ✓ | Final price per unit after discounts |
external_product_id | string | Your product SKU / ID | |
product_category | string | Product category | |
unit_buying_price | decimal | Cost price per unit (for margin tracking) | |
unit_discount | decimal | Discount per unit | |
meta | object | Per-item custom metadata |
Response 201 Created
{
"success": true,
"bill_token": "pv_b_a1b2c3d4e5f6...",
"pay_url": "https://payvault.trialvo.com/pay/pv_b_a1b2c3d4e5f6...",
"expires_at": "2026-06-22T08:30:00+00:00"
}
Bills expire after a configurable period (default: 30 minutes). Once expired, the customer can no longer pay.
Get Bill Status
Retrieve the current status of a bill. Use this to verify payment status after receiving an IPN notification.
For GET requests with no body, set X-Body-Hash to e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 (SHA-256 of empty string).
Bill Statuses
| Status | Description |
|---|---|
| pending | Bill created, awaiting payment |
| processing | Payment initiated, waiting for gateway response |
| paid | Payment completed successfully |
| failed | Payment attempt failed |
| cancelled | Bill was cancelled before payment |
| expired | Bill expired before payment (configurable timeout) |
| refunded | Payment was fully refunded |
Cancel Bill
Cancel a pending bill. Only bills in pending status can be cancelled. Cancelling a bill that is already paid, failed, or expired will return a 400 error.
Response 200 OK
{
"success": true,
"message": "Bill cancelled"
}
Request Refund
Submit a refund request for a paid bill. All refunds require manual admin approval before being processed — they are not automatic.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
bill_token | string | ✓ | The bill token of the paid bill |
refund_amount | decimal | ✓ | Amount to refund (must be > 0 and ≤ bill amount) |
refund_reason | string | ✓ | Reason for the refund |
refund_type | string | "full" or "partial" — auto-detected from amount if omitted | |
external_ref | string | Your internal reference for this refund |
Business Rules
- Only bills with status
paidorpartially_paidcan be refunded - Refunds are rejected if the bill is older than the configured refund window (default: 30 days)
- Refund amount cannot exceed the bill's
final_amount
Response 201 Created
{
"success": true,
"refund_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "requested",
"message": "Refund request submitted. Pending admin approval."
}
Get Refund Status
Check the status of a refund request.
Refund Statuses
| Status | Description |
|---|---|
| requested | Refund submitted, pending admin review |
| approved | Admin approved — refund is being processed |
| rejected | Admin rejected the refund request |
| completed | Refund processed and money returned |
Get Transaction
Retrieve details of a payment transaction, including its event history (initiated, callback, success/failure).
Response 200 OK
{
"transaction": {
"id": "uuid",
"bill_id": "uuid",
"eps_merchant_tx_id": "PV_20260622_abc123",
"status": "success",
"amount": "1500.00",
"currency": "BDT",
"created_at": "2026-06-22T07:20:00Z"
},
"events": [
{ "event_type": "initiated", "created_at": "2026-06-22T07:18:00Z" },
{ "event_type": "callback_received", "created_at": "2026-06-22T07:20:00Z" },
{ "event_type": "success", "created_at": "2026-06-22T07:20:01Z" }
]
}
IPN Webhooks
When a payment status changes, PayVault sends a POST request to your registered IPN (Instant Payment Notification) endpoint. IPN endpoints are configured by your admin via the admin panel.
IPN Event Types
| Event | Triggered When |
|---|---|
payment.success | Customer completed payment successfully |
payment.failed | Payment attempt was declined or errored |
payment.cancelled | Customer cancelled on the payment page |
refund.approved | Admin approved a refund request |
refund.completed | Refund has been processed |
IPN Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-PayVault-Signature | HMAC-SHA256 signature of the raw body, using your IPN secret |
X-PayVault-Event | Always ipn |
IPN Payload Format
The exact fields in the IPN payload for payment events (based on the actual server code):
{
"event": "payment.success",
"bill_token": "pv_b_a1b2c3d4e5f6...",
"transaction_id": "PV_20260622_abc123",
"eps_transaction_id": "EPS_TXN_789",
"amount": "1500.00",
"currency": "BDT",
"status": "paid",
"financial_entity": "bKash",
"customer_id": "CUST_123",
"payment_reference": "REF_456",
"transaction_date": "2026-06-22 07:20:00",
"external_order_id": "ORD-001",
"external_subscription_id": null,
"value_a": "...",
"value_b": "...",
"value_c": "...",
"timestamp": "2026-06-22T07:20:01+00:00"
}
bill_token — use to look up the original order in your system. external_order_id — your order ID as submitted. status — the bill's new status. financial_entity — which payment method was used (bKash, Nagad, etc.).
IPN Delivery & Retries
- PayVault sends the IPN immediately after the payment callback
- If your endpoint returns a non-2xx status, PayVault retries with exponential backoff
- Your endpoint must respond with HTTP
200within 10 seconds - Failed deliveries are queued and retried automatically
Verify IPN Signatures
Every IPN request includes an X-PayVault-Signature header. You must verify this signature to ensure the webhook is genuine and hasn't been tampered with.
expected_signature = HMAC-SHA256(your_ipn_secret, raw_request_body) → hex
// Compare expected_signature with X-PayVault-Signature header (constant-time comparison)
Without signature verification, an attacker could send fake payment.success notifications to your endpoint, tricking your system into fulfilling orders that were never paid for.
Integration Examples
Complete working examples for creating a bill. Copy and adapt to your project.
Pythonimport hmac, hashlib, time, uuid, json, requests # ─── Your credentials (store in env vars!) ─────────────────────── SERVICE_ID = "your-service-uuid" # From admin panel → Services API_KEY = "your-64-char-hex-api-key" # From admin panel → Reveal Key BASE_URL = "https://payvault.trialvo.com/api/v1" def payvault_request(method, path, body_dict=None): """Make a signed request to the PayVault API.""" body = json.dumps(body_dict) if body_dict else "" timestamp = str(int(time.time())) nonce = str(uuid.uuid4()) body_hash = hashlib.sha256(body.encode()).hexdigest() message = f"{SERVICE_ID}:{timestamp}:{nonce}:{body_hash}" signature = hmac.new( API_KEY.encode(), message.encode(), hashlib.sha256 ).hexdigest() headers = { "Content-Type": "application/json", "X-Service-Id": SERVICE_ID, "X-Api-Key": API_KEY, "X-Timestamp": timestamp, "X-Nonce": nonce, "X-Body-Hash": body_hash, "X-Signature": signature, } resp = requests.request(method, f"{BASE_URL}{path}", headers=headers, data=body or None) resp.raise_for_status() return resp.json() # ─── Create a bill ─────────────────────────────────────────────── bill = payvault_request("POST", "/bills", { "customer_name": "Rahim Ahmed", "customer_email": "rahim@example.com", "customer_phone": "+8801712345678", "customer_address": "123 Gulshan Ave", "customer_city": "Dhaka", "customer_state": "Dhaka", "customer_postcode": "1212", "subtotal": "1500.00", "final_amount": "1500.00", "external_order_id": "ORD-001", "success_url": "https://yoursite.com/payment/success", "fail_url": "https://yoursite.com/payment/failed", "cancel_url": "https://yoursite.com/payment/cancelled", "items": [{ "product_name": "Premium Plan", "quantity": 1, "unit_selling_price": "1500.00", "unit_final_price": "1500.00", }] }) print(f"Redirect customer to: {bill['pay_url']}") # ─── Check bill status (GET — no body) ────────────────────────── status = payvault_request("GET", f"/bills/{bill['bill_token']}") print(f"Bill status: {status['status']}")
Node.jsconst crypto = require('crypto'); // ─── Your credentials (store in env vars!) ─────────────────────── const SERVICE_ID = 'your-service-uuid'; const API_KEY = 'your-64-char-hex-api-key'; const BASE_URL = 'https://payvault.trialvo.com/api/v1'; async function payvaultRequest(method, path, bodyObj = null) { const body = bodyObj ? JSON.stringify(bodyObj) : ''; const timestamp = Math.floor(Date.now() / 1000).toString(); const nonce = crypto.randomUUID(); const bodyHash = crypto.createHash('sha256').update(body).digest('hex'); const message = `${SERVICE_ID}:${timestamp}:${nonce}:${bodyHash}`; const signature = crypto.createHmac('sha256', API_KEY) .update(message).digest('hex'); const res = await fetch(`${BASE_URL}${path}`, { method, headers: { 'Content-Type': 'application/json', 'X-Service-Id': SERVICE_ID, 'X-Api-Key': API_KEY, 'X-Timestamp': timestamp, 'X-Nonce': nonce, 'X-Body-Hash': bodyHash, 'X-Signature': signature, }, body: body || undefined, }); if (!res.ok) { const text = await res.text(); throw new Error(`PayVault ${res.status}: ${text}`); } return res.json(); } // ─── Create a bill ─────────────────────────────────────────────── const bill = await payvaultRequest('POST', '/bills', { customer_name: 'Rahim Ahmed', customer_email: 'rahim@example.com', customer_phone: '+8801712345678', customer_address: '123 Gulshan Ave', customer_city: 'Dhaka', customer_state: 'Dhaka', customer_postcode: '1212', subtotal: '1500.00', final_amount: '1500.00', external_order_id: 'ORD-001', success_url: 'https://yoursite.com/payment/success', fail_url: 'https://yoursite.com/payment/failed', cancel_url: 'https://yoursite.com/payment/cancelled', items: [{ product_name: 'Premium Plan', quantity: 1, unit_selling_price: '1500.00', unit_final_price: '1500.00', }] }); console.log('Redirect customer to:', bill.pay_url); // ─── Check bill status ────────────────────────────────────────── const status = await payvaultRequest('GET', `/bills/${bill.bill_token}`); console.log('Bill status:', status.status);
PHP<?php // ─── Your credentials (store in env vars!) ─────────────────────── $SERVICE_ID = 'your-service-uuid'; $API_KEY = 'your-64-char-hex-api-key'; $BASE_URL = 'https://payvault.trialvo.com/api/v1'; function payvaultRequest($method, $path, $bodyObj = null) { global $SERVICE_ID, $API_KEY, $BASE_URL; $body = $bodyObj ? json_encode($bodyObj) : ''; $timestamp = (string) time(); $nonce = bin2hex(random_bytes(16)); $bodyHash = hash('sha256', $body); $message = "$SERVICE_ID:$timestamp:$nonce:$bodyHash"; $signature = hash_hmac('sha256', $message, $API_KEY); $ch = curl_init("$BASE_URL$path"); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_POSTFIELDS => $body ?: null, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 30, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', "X-Service-Id: $SERVICE_ID", "X-Api-Key: $API_KEY", "X-Timestamp: $timestamp", "X-Nonce: $nonce", "X-Body-Hash: $bodyHash", "X-Signature: $signature", ], ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode >= 400) { throw new Exception("PayVault error ($httpCode): $response"); } return json_decode($response, true); } // ─── Create a bill ─────────────────────────────────────────────── $bill = payvaultRequest('POST', '/bills', [ 'customer_name' => 'Rahim Ahmed', 'customer_email' => 'rahim@example.com', 'customer_phone' => '+8801712345678', 'customer_address' => '123 Gulshan Ave', 'customer_city' => 'Dhaka', 'customer_state' => 'Dhaka', 'customer_postcode' => '1212', 'subtotal' => '1500.00', 'final_amount' => '1500.00', 'external_order_id' => 'ORD-001', 'success_url' => 'https://yoursite.com/payment/success', 'fail_url' => 'https://yoursite.com/payment/failed', 'cancel_url' => 'https://yoursite.com/payment/cancelled', 'items' => [[ 'product_name' => 'Premium Plan', 'quantity' => 1, 'unit_selling_price' => '1500.00', 'unit_final_price' => '1500.00', ]] ]); echo "Redirect customer to: " . $bill['pay_url'] . "\n"; ?>
Bash / cURL# ─── Step 1: Set your credentials ──────────────────────────────── SERVICE_ID="your-service-uuid" API_KEY="your-64-char-hex-api-key" BASE_URL="https://payvault.trialvo.com/api/v1" # ─── Step 2: Prepare the request body ──────────────────────────── BODY='{"customer_name":"Rahim Ahmed","customer_email":"rahim@example.com","customer_phone":"+8801712345678","customer_address":"123 Gulshan Ave","customer_city":"Dhaka","customer_state":"Dhaka","customer_postcode":"1212","subtotal":"1500.00","final_amount":"1500.00","external_order_id":"ORD-001","success_url":"https://yoursite.com/payment/success","fail_url":"https://yoursite.com/payment/failed","cancel_url":"https://yoursite.com/payment/cancelled","items":[{"product_name":"Premium Plan","quantity":1,"unit_selling_price":"1500.00","unit_final_price":"1500.00"}]}' # ─── Step 3: Compute authentication values ─────────────────────── TIMESTAMP=$(date +%s) NONCE=$(uuidgen || python3 -c "import uuid; print(uuid.uuid4())") BODY_HASH=$(echo -n "$BODY" | sha256sum | awk '{print $1}') MESSAGE="${SERVICE_ID}:${TIMESTAMP}:${NONCE}:${BODY_HASH}" SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$API_KEY" | awk '{print $2}') # ─── Step 4: Make the request ──────────────────────────────────── curl -X POST "${BASE_URL}/bills" \ -H "Content-Type: application/json" \ -H "X-Service-Id: ${SERVICE_ID}" \ -H "X-Api-Key: ${API_KEY}" \ -H "X-Timestamp: ${TIMESTAMP}" \ -H "X-Nonce: ${NONCE}" \ -H "X-Body-Hash: ${BODY_HASH}" \ -H "X-Signature: ${SIGNATURE}" \ -d "$BODY"
IPN Verification Examples
Verify incoming webhooks in your IPN handler. Always verify before processing.
Python (Flask)import hmac, hashlib from flask import Flask, request, jsonify IPN_SECRET = "your-ipn-secret" # From admin panel → IPN Endpoints app = Flask(__name__) @app.route("/webhook/payvault", methods=["POST"]) def payvault_ipn(): # 1. Get the raw body and signature header raw_body = request.get_data(as_text=True) signature = request.headers.get("X-PayVault-Signature", "") # 2. Compute expected signature expected = hmac.new( IPN_SECRET.encode(), raw_body.encode(), hashlib.sha256 ).hexdigest() # 3. Verify (constant-time comparison) if not hmac.compare_digest(expected, signature): return jsonify({"error": "Invalid signature"}), 403 # 4. Process the event data = request.get_json() event = data["event"] bill_token = data["bill_token"] if event == "payment.success": # Fulfill the order in your system print(f"Payment success for bill {bill_token}") # TODO: update order status, send confirmation email, etc. elif event == "payment.failed": print(f"Payment failed for bill {bill_token}") return jsonify({"received": True}), 200
Node.js (Express)const crypto = require('crypto'); const express = require('express'); const app = express(); const IPN_SECRET = 'your-ipn-secret'; // IMPORTANT: Use raw body for signature verification app.post('/webhook/payvault', express.raw({ type: 'application/json' }), (req, res) => { const rawBody = req.body.toString('utf8'); const signature = req.headers['x-payvault-signature'] || ''; // Compute expected signature const expected = crypto.createHmac('sha256', IPN_SECRET) .update(rawBody).digest('hex'); // Constant-time comparison if (!crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex') )) { return res.status(403).json({ error: 'Invalid signature' }); } // Process the event const data = JSON.parse(rawBody); if (data.event === 'payment.success') { console.log(`Payment success for ${data.bill_token}`); // TODO: fulfill order } res.json({ received: true }); } );
PHP<?php $IPN_SECRET = 'your-ipn-secret'; // 1. Read raw body $rawBody = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_PAYVAULT_SIGNATURE'] ?? ''; // 2. Compute expected signature $expected = hash_hmac('sha256', $rawBody, $IPN_SECRET); // 3. Verify (constant-time comparison) if (!hash_equals($expected, $signature)) { http_response_code(403); echo json_encode(['error' => 'Invalid signature']); exit; } // 4. Process the event $data = json_decode($rawBody, true); if ($data['event'] === 'payment.success') { // Fulfill the order error_log("Payment success for bill " . $data['bill_token']); // TODO: update order status } http_response_code(200); echo json_encode(['received' => true]); ?>
Error Handling
All error responses follow a consistent JSON format:
{ "error": "Human-readable error message" }
HTTP Status Codes
| Code | Meaning | Common Causes |
|---|---|---|
201 | Created | Bill or refund created successfully |
200 | OK | Request successful |
400 | Bad Request | Invalid parameters, business logic error (e.g. refunding an unpaid bill) |
401 | Unauthorized | Missing headers, invalid API key, expired timestamp, bad signature, reused nonce |
403 | Forbidden | Trying to access another service's bill/refund/transaction |
404 | Not Found | Bill token, refund ID, or transaction ID doesn't exist |
500 | Server Error | Internal error — retry with exponential backoff |
Troubleshooting 401 Errors
| Error Message | Cause | Fix |
|---|---|---|
Missing X-Service-Id header | Header not sent | Include all 7 required headers |
Invalid API key | Key not found in database | Verify you're using the full key, not just the prefix |
Request timestamp expired | Clock skew > 5 minutes | Sync your server clock with NTP |
Nonce already used | Same nonce sent twice | Generate a new UUID for every request |
Invalid HMAC signature | Signature mismatch | Check message format: svc_id:ts:nonce:body_hash. Verify API key is UTF-8 encoded. |
Service is deactivated | Admin disabled your service | Contact your PayVault admin |
Service ID mismatch | X-Service-Id doesn't match the key's service | Ensure service ID and key belong to the same service |
Retry Strategy
For 500 errors, implement exponential backoff:
- Wait 1s, then retry
- Wait 2s, then retry
- Wait 4s, then retry
- Maximum 3 retries. If still failing, log and alert.
For 401 and 400 errors, do not retry — fix the request first.
Contact your PayVault administrator for API credentials, IPN endpoint setup, and technical support. For self-hosted deployments, refer to the project README.