Developer documentation for the OpenPay payment processing API
The OpenPay Public API allows merchants to programmatically create payment links, retrieve transaction details, and integrate OpenPay directly into their applications.
All API requests must be authenticated using an API Key. You can generate API keys in the Merchant Dashboard under Settings > Developer API.
Include your API key in the x-api-key header of each request.
x-api-key: sk_live_...
https://api.openpay.bz/v1
Paylinks are secure, shareable URLs that allow customers to make payments.
Creates a new payment link.
Endpoint: POST /paylinks
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
amount |
number | Yes | Payment amount (e.g., 150.00) |
currency |
string | Yes | Currency code (e.g., "BZD", "USD") |
description |
string | No | Description of the payment |
customer_name |
string | No | Name of the customer |
customer_email |
string | No | Email of the customer for receipts |
return_url |
string | No | URL to redirect customer after successful payment |
Example Request:
curl -X POST https://api.openpay.bz/v1/paylinks \
-H "x-api-key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 150.00,
"currency": "BZD",
"description": "Invoice #1234",
"customer_name": "John Doe",
"customer_email": "john@example.com",
"return_url": "https://myshop.com/success"
}'
Success Response (201 Created):
{
"success": true,
"data": {
"id": "cafe62fc-11e5-4000-a11b-e95f0f5f54e7",
"url": "https://merchant.openpay.bz/payment/cafe62fc-11e5-4000-a11b-e95f0f5f54e7",
"qr_code": "https://api.openpay.bz/qr/cafe62fc-11e5-4000-a11b-e95f0f5f54e7.png",
"amount": 150.00,
"currency": "BZD",
"status": "active",
"created_at": "2026-02-11T09:30:43.473Z"
}
}
The qr_code field is a URL to a PNG image that encodes the paylink URL — useful for printing on receipts or displaying on a checkout screen.
Error Response — Currency not enabled (400 Bad Request):
The shape of this error differs between the two API surfaces:
/v1 API-key surface (POST /v1/paylinks — this document):
{
"success": false,
"error": "currency_not_enabled",
"message": "USD is not enabled for this merchant — no USD gateway configuration"
}
Authenticated portal surface (POST /api/paylinks, PUT /api/paylinks/:id, PATCH /api/paylinks/:id):
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"currency": "USD is not enabled for this merchant — no USD gateway configuration"
}
}
}
This error is returned when the requested currency does not have a complete gateway credential slot configured for the merchant. Check the enabled_currencies field on the merchant profile (GET /me) to know which currencies are available before creating paylinks.
Retrieves details of a specific paylink.
Endpoint: GET /paylinks/:id
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | The ID of the paylink to retrieve |
Example Request:
curl -X GET https://api.openpay.bz/v1/paylinks/cafe62fc-11e5-4000-a11b-e95f0f5f54e7 \
-H "x-api-key: sk_live_..."
Success Response (200 OK):
{
"success": true,
"data": {
"id": "cafe62fc-11e5-4000-a11b-e95f0f5f54e7",
"url": "https://merchant.openpay.bz/payment/cafe62fc-11e5-4000-a11b-e95f0f5f54e7",
"qr_code": "https://api.openpay.bz/qr/cafe62fc-11e5-4000-a11b-e95f0f5f54e7.png",
"amount": 150.00,
"currency": "BZD",
"status": "active",
"description": "Invoice #1234",
"customer_name": "John Doe",
"created_at": "2026-02-11T09:30:43.473Z"
}
}
Webhooks let you receive real-time notifications of payment outcomes without polling the /transactions or /paylinks endpoints. When an event occurs — a paylink is paid, a payment fails, or a refund is issued — OpenPay immediately POSTs a signed JSON payload to each active endpoint you have registered.
POST /v1/webhooks.POST request to that URL whenever a matching event fires.2xx status to acknowledge receipt.OpenPay emits five subscribable event types. Each delivery is an event envelope with the shape:
{
"id": "evt_<32 hex chars>",
"type": "<event.type>",
"created": 1754000000,
"data": { ... }
}
| Field | Type | Description |
|---|---|---|
id |
string | Unique event ID (evt_ + 32 lowercase hex chars). Use this to deduplicate — deliveries are at-least-once. |
type |
string | One of the five event types below. |
created |
number | Unix timestamp (seconds) when the event was built. |
data |
object | Event-specific payload (see below). |
paylink.paidEmitted when a customer completes payment through a paylink.
{
"id": "evt_3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"type": "paylink.paid",
"created": 1754001234,
"data": {
"transaction_id": "25501adf-48e0-41b7-bbbc-c831de450dd0",
"display_id": "OPY-00042",
"status": "completed",
"amount": 150.00,
"currency": "BZD",
"paylink_id": "cafe62fc-11e5-4000-a11b-e95f0f5f54e7",
"channel": "web",
"paid_via_wallet": false
}
}
payment.completedEmitted when a direct API payment (not paylink-sourced) is processed successfully.
{
"id": "evt_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"type": "payment.completed",
"created": 1754001300,
"data": {
"transaction_id": "b7e12abc-0001-4001-8001-a00000000001",
"display_id": "OPY-00043",
"status": "completed",
"amount": 50.00,
"currency": "BZD",
"paylink_id": null,
"channel": null,
"paid_via_wallet": false
}
}
payment.failedEmitted when a payment attempt is declined or encounters a gateway error. No card data or raw gateway response is included.
{
"id": "evt_9f8e7d6c5b4a3b2c1d0e9f8e7d6c5b4a",
"type": "payment.failed",
"created": 1754001400,
"data": {
"transaction_id": "c3d45678-0002-4002-8002-b00000000002",
"status": "failed",
"amount": 75.00,
"currency": "BZD",
"paylink_id": "cafe62fc-11e5-4000-a11b-e95f0f5f54e7",
"response_code": "INSUFFICIENT_FUNDS",
"error_message": "Insufficient funds"
}
}
paylink_id is omitted from the data object when the payment was not paylink-sourced.
refund.completedEmitted after a full refund is successfully processed.
{
"id": "evt_2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e",
"type": "refund.completed",
"created": 1754002000,
"data": {
"transaction_id": "25501adf-48e0-41b7-bbbc-c831de450dd0",
"status": "refunded",
"amount": 150.00,
"original_amount": 150.00,
"currency": "BZD",
"refund_id": "REF-8291038",
"is_partial": false,
"reason": "Customer request"
}
}
refund.partialEmitted after a partial refund is successfully processed.
{
"id": "evt_4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f",
"type": "refund.partial",
"created": 1754002100,
"data": {
"transaction_id": "25501adf-48e0-41b7-bbbc-c831de450dd0",
"status": "partially_refunded",
"amount": 50.00,
"original_amount": 150.00,
"currency": "BZD",
"refund_id": "REF-8291039",
"is_partial": true,
"reason": "Partial cancellation"
}
}
OpenPay delivers events at-least-once. Network retries or delivery worker restarts may cause the same event to arrive more than once. Always deduplicate on the id field before applying business logic.
Every delivery request carries an X-OpenPay-Signature header. Verify it to confirm the payload came from OpenPay and has not been tampered with.
X-OpenPay-Signature: t=1754001234,v1=a1b2c3d4...
| Part | Description |
|---|---|
t |
Unix timestamp (seconds) when the request was signed. |
v1 |
HMAC-SHA256 hex digest — see construction below. |
signed_payload = "<t>.<raw_request_body>"
v1 = HMAC_SHA256(signing_secret, signed_payload)
<t> is the literal decimal string from the t= part of the header.<raw_request_body> is the exact bytes received over the network — do not parse and re-serialize, as whitespace differences will break the signature.signing_secret is the full whsec_<48 hex chars> string shown when you created or rotated the endpoint.Reject any delivery whose timestamp t is more than 5 minutes in the past (or future). This prevents replay attacks.
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret) {
// Parse header: "t=1754001234,v1=abc123..."
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const t = parts['t'];
const v1 = parts['v1'];
if (!t || !v1) throw new Error('Missing signature parts');
// Replay protection: reject if timestamp is older than 5 minutes
const nowSeconds = Math.floor(Date.now() / 1000);
if (Math.abs(nowSeconds - parseInt(t, 10)) > 300) {
throw new Error('Timestamp too old — possible replay attack');
}
// Compute expected signature
const signedPayload = `${t}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('hex');
// Constant-time comparison to prevent timing attacks
const expectedBuf = Buffer.from(expected, 'hex');
const receivedBuf = Buffer.from(v1, 'hex');
if (
expectedBuf.length !== receivedBuf.length ||
!crypto.timingSafeEqual(expectedBuf, receivedBuf)
) {
throw new Error('Signature mismatch');
}
return true; // Signature valid
}
// Express usage (use express.raw() for the webhook route, not express.json())
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
verifyWebhook(
req.body.toString('utf8'),
req.headers['x-openpay-signature'],
process.env.OPENPAY_WEBHOOK_SECRET
);
// Process event...
res.sendStatus(200);
});
Important: You must verify over the raw request body bytes, not a re-serialized JSON object. Use a body-parsing middleware that preserves the raw buffer (e.g.,
express.raw()), notexpress.json().
import hashlib
import hmac
import time
def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
# Parse header: "t=1754001234,v1=abc123..."
parts = dict(p.split('=', 1) for p in signature_header.split(','))
t = parts.get('t')
v1 = parts.get('v1')
if not t or not v1:
raise ValueError('Missing signature parts')
# Replay protection: reject if timestamp is older than 5 minutes
now = int(time.time())
if abs(now - int(t)) > 300:
raise ValueError('Timestamp too old — possible replay attack')
# Compute expected signature over raw body bytes
signed_payload = f'{t}.'.encode() + raw_body
expected = hmac.new(
secret.encode('utf-8'),
signed_payload,
hashlib.sha256
).hexdigest()
# Constant-time comparison
if not hmac.compare_digest(expected, v1):
raise ValueError('Signature mismatch')
return True
# Flask usage
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
verify_webhook(
request.get_data(), # raw bytes — do not call request.json
request.headers.get('X-OpenPay-Signature', ''),
os.environ['OPENPAY_WEBHOOK_SECRET']
)
event = request.get_json()
# Process event...
return '', 200
| Header | Description |
|---|---|
Content-Type |
application/json |
X-OpenPay-Signature |
t=<unixSeconds>,v1=<hex> — see above |
X-OpenPay-Event |
The event type, e.g. paylink.paid |
X-OpenPay-Delivery |
Unique delivery ID for this attempt |
If your endpoint returns a non-2xx status or the connection times out, OpenPay retries the delivery automatically.
| Attempt | Delay after previous failure |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
After 6 failed attempts the delivery is dead-lettered (status: "failed") and no further retries are made. Each POST attempt has a 10-second timeout. If your endpoint is temporarily down, fix it and use POST /v1/webhooks/:id/test to send a fresh test event once it recovers — dead-lettered deliveries are not replayed.
A 2xx response — any status from 200 through 299 — is treated as success. The response body is ignored.
All webhook management endpoints are under /v1/webhooks and require your API key in the x-api-key header. Endpoints are scoped to your merchant account; you cannot see or modify another merchant's webhooks.
The signing secret is shown in full only on POST /v1/webhooks (create) and POST /v1/webhooks/:id/rotate-secret. All other read responses return the secret masked as whsec_…<last4>.
POST /v1/webhooks
| Field | Type | Required | Description |
|---|---|---|---|
url |
string | Yes | HTTPS URL to receive events |
event_types |
array | No | List of event types to subscribe to. Omit or pass [] to subscribe to all events. |
description |
string | No | Human-readable label for this endpoint |
curl -X POST https://api.openpay.bz/v1/webhooks \
-H "x-api-key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://myshop.com/webhooks/openpay",
"event_types": ["paylink.paid", "refund.completed", "refund.partial"],
"description": "Production payment notifications"
}'
Success Response (201 Created):
{
"success": true,
"data": {
"id": "wh_01234567-89ab-cdef-0123-456789abcdef",
"url": "https://myshop.com/webhooks/openpay",
"signing_secret": "whsec_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6",
"event_types": ["paylink.paid", "refund.completed", "refund.partial"],
"description": "Production payment notifications",
"is_active": true,
"created_at": "2026-08-03T10:00:00.000Z",
"updated_at": "2026-08-03T10:00:00.000Z"
}
}
Save signing_secret immediately — it is not shown again.
GET /v1/webhooks
curl https://api.openpay.bz/v1/webhooks \
-H "x-api-key: sk_live_..."
Success Response (200 OK):
{
"success": true,
"data": [
{
"id": "wh_01234567-89ab-cdef-0123-456789abcdef",
"url": "https://myshop.com/webhooks/openpay",
"signing_secret": "whsec_…cdef",
"event_types": ["paylink.paid", "refund.completed", "refund.partial"],
"description": "Production payment notifications",
"is_active": true,
"created_at": "2026-08-03T10:00:00.000Z",
"updated_at": "2026-08-03T10:00:00.000Z"
}
]
}
GET /v1/webhooks/:id
curl https://api.openpay.bz/v1/webhooks/wh_01234567-89ab-cdef-0123-456789abcdef \
-H "x-api-key: sk_live_..."
PATCH /v1/webhooks/:id
All fields are optional; only the fields you send are updated.
| Field | Type | Description |
|---|---|---|
url |
string | New HTTPS destination URL |
event_types |
array | New event type subscription list ([] = all) |
description |
string | New label |
is_active |
boolean | false to pause delivery without deleting the endpoint |
curl -X PATCH https://api.openpay.bz/v1/webhooks/wh_01234567-89ab-cdef-0123-456789abcdef \
-H "x-api-key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{"is_active": false}'
Success Response (200 OK):
{
"success": true,
"data": {
"id": "wh_01234567-89ab-cdef-0123-456789abcdef",
"url": "https://myshop.com/webhooks/openpay",
"signing_secret": "whsec_…cdef",
"event_types": ["paylink.paid", "refund.completed", "refund.partial"],
"description": "Production payment notifications",
"is_active": false,
"created_at": "2026-08-03T10:00:00.000Z",
"updated_at": "2026-08-03T10:15:00.000Z"
}
}
DELETE /v1/webhooks/:id
Returns 204 No Content on success.
curl -X DELETE https://api.openpay.bz/v1/webhooks/wh_01234567-89ab-cdef-0123-456789abcdef \
-H "x-api-key: sk_live_..."
POST /v1/webhooks/:id/rotate-secret
Generates a new signing secret for the endpoint. The previous secret is immediately invalidated. Update your verification logic before rotating in production.
curl -X POST https://api.openpay.bz/v1/webhooks/wh_01234567-89ab-cdef-0123-456789abcdef/rotate-secret \
-H "x-api-key: sk_live_..."
Success Response (200 OK):
{
"success": true,
"data": {
"id": "wh_01234567-89ab-cdef-0123-456789abcdef",
"url": "https://myshop.com/webhooks/openpay",
"signing_secret": "whsec_b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1",
"event_types": ["paylink.paid", "refund.completed", "refund.partial"],
"description": "Production payment notifications",
"is_active": true,
"created_at": "2026-08-03T10:00:00.000Z",
"updated_at": "2026-08-03T10:30:00.000Z"
}
}
POST /v1/webhooks/:id/test
Enqueues a webhook.test event to the endpoint immediately. Use this to confirm your server is reachable and your signature verification is working. Rate-limited to 5 requests per 60 seconds per merchant.
curl -X POST https://api.openpay.bz/v1/webhooks/wh_01234567-89ab-cdef-0123-456789abcdef/test \
-H "x-api-key: sk_live_..."
Success Response (200 OK):
{
"success": true,
"data": {
"delivery_id": "dlv_fedcba9876543210fedcba9876543210",
"event_id": "evt_0123456789abcdef0123456789abcdef",
"event_type": "webhook.test",
"status": "pending"
}
}
The test payload your endpoint receives looks like:
{
"id": "evt_0123456789abcdef0123456789abcdef",
"type": "webhook.test",
"created": 1754001234,
"data": {
"message": "This is a test webhook event sent from the OpenPay API."
}
}
webhook.test is not a subscribable event type — it is sent regardless of your event_types subscription list.
GET /v1/webhook-deliveries
Returns recent delivery attempts across all your endpoints. Useful for debugging failed deliveries.
| Query parameter | Description |
|---|---|
endpoint_id |
Filter by a specific endpoint ID |
status |
Filter by status: pending, delivered, or failed |
limit |
Maximum number of results to return |
curl "https://api.openpay.bz/v1/webhook-deliveries?status=failed&limit=20" \
-H "x-api-key: sk_live_..."
Success Response (200 OK):
{
"success": true,
"data": [
{
"id": "dlv_fedcba9876543210fedcba9876543210",
"endpoint_id": "wh_01234567-89ab-cdef-0123-456789abcdef",
"event_id": "evt_3a1b2c4d5e6f7a8b9c0d1e2f3a4b5c6d",
"event_type": "paylink.paid",
"status": "failed",
"attempts": 6,
"last_http_status": 503,
"last_error": "HTTP 503: Service Unavailable",
"delivered_at": null,
"created_at": "2026-08-03T09:00:00.000Z",
"updated_at": "2026-08-03T15:00:00.000Z"
}
]
}
Process payments directly through the API without creating a paylink first. This is ideal for integrating payment processing into your application (e.g., food ordering apps, e-commerce checkouts).
Processes a direct payment with card details.
Endpoint: POST /payments
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
amount |
number | Yes | Payment amount (e.g., 50.00) |
currency |
string | Yes | Currency code (e.g., "BZD", "USD") |
card_number |
string | Yes | Full card number (16 digits) |
card_expiry_month |
string | Yes | Card expiry month (e.g., "12") |
card_expiry_year |
string | Yes | Card expiry year (e.g., "2025") |
card_cvv |
string | Yes | Card CVV/CVC code (3-4 digits) |
card_holder_name |
string | Yes | Name on the card |
description |
string | No | Description of the payment |
customer_name |
string | No | Name of the customer |
customer_email |
string | No | Email of the customer for receipts |
Example Request:
curl -X POST https://api.openpay.bz/v1/payments \
-H "x-api-key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 50.00,
"currency": "BZD",
"card_number": "4541250000000001",
"card_expiry_month": "12",
"card_expiry_year": "2025",
"card_cvv": "123",
"card_holder_name": "John Doe",
"description": "Order #5678",
"customer_name": "John Doe",
"customer_email": "john@example.com"
}'
Success Response (200 OK):
{
"success": true,
"data": {
"transaction_id": "25501adf-48e0-41b7-bbbc-c831de450dd0",
"status": "completed",
"message": "Payment processed successfully",
"gateway_reference": "BPREF-609286",
"response_code": "APPROVED"
}
}
Verification Required Response (402 Payment Required):
When a new card is used for the first time, verification is required:
{
"success": false,
"error_code": "VERIFICATION_INITIATED",
"message": "Card verification required. A small random amount has been charged to your card. Please verify the amount to continue.",
"verification_required": true,
"verification_currency": "USD"
}
verification_currency ("USD" or "BZD") is the currency the micro-charge
was made in. Show it to the cardholder: banks outside that currency zone
display the charge converted, so the customer must enter the original
verification_currency amount (usually visible in the transaction details of
their banking app), not the converted figure.
Error Response (400 Bad Request):
{
"success": false,
"message": "Invalid card",
"response_code": "INVALID_CARD"
}
Error Response — currency not enabled (400 Bad Request):
{
"success": false,
"error": "currency_not_enabled",
"message": "USD is not enabled for this merchant — no USD gateway configuration"
}
Returned when the requested currency is not configured for your gateway. Check your gateway settings or use a supported currency.
Rate-Limited / Abuse Block (429 Too Many Requests):
OpenPay's abuse-detection layer transparently rejects requests that look like card-enumeration or velocity attacks. The card, IP, or merchant is blocked for a cooldown window; legitimate retries can resume once the window passes. The message describes the specific block (e.g. too many declines on the same card, or too many cards from the same IP).
{
"success": false,
"message": "Too many declined attempts. Please try again later."
}
Test-mode tagging. When the merchant is in test status, every response branch above (200, 402, 400, 429) additionally carries "test_mode": true at the top level. See Test Mode for the simulator behavior.
Retrieve a paginated list of your transactions with optional filters. Useful for reconciliation, reporting, and monitoring payment activity.
Endpoint: GET /transactions
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
page |
number | No | Page number (default: 1) |
limit |
number | No | Results per page, max 100 (default: 20) |
status |
string | No | Filter by status: pending, completed, failed, refunded |
currency |
string | No | Filter by currency: BZD or USD |
start_date |
string | No | Filter from date (ISO 8601, e.g., 2026-04-01T00:00:00Z) |
end_date |
string | No | Filter to date (ISO 8601) |
paylink_id |
string | No | Filter by paylink UUID |
transaction_type |
string | No | Filter by type: payment, verification, refund, chargeback |
Example Request:
curl -X GET "https://api.openpay.bz/v1/transactions?status=completed&start_date=2026-04-01&limit=50" \
-H "x-api-key: sk_live_..."
Success Response (200 OK):
{
"success": true,
"data": [
{
"id": "abc-123-...",
"display_id": "TX1234567890",
"amount": 50.00,
"currency": "BZD",
"status": "completed",
"transaction_type": "payment",
"payment_processor": "plug_n_pay",
"card_type": "Visa",
"card_last_four": "1234",
"reference_number": "TX1234567890",
"created_at": "2026-04-15T14:30:00.000Z",
"updated_at": "2026-04-15T14:30:02.000Z",
"metadata": {
"customer_name": "John Doe",
"authorization_code": "AUTH123",
"gateway_reference": "2026041514300000001"
}
}
],
"pagination": {
"total": 142,
"page": 1,
"limit": 50,
"total_pages": 3
}
}
Retrieve details for a single transaction by ID.
Endpoint: GET /transactions/:id
Example Request:
curl -X GET "https://api.openpay.bz/v1/transactions/abc-123-..." \
-H "x-api-key: sk_live_..."
Success Response (200 OK):
{
"success": true,
"data": {
"id": "abc-123-...",
"amount": 50.00,
"currency": "BZD",
"status": "completed",
"payment_processor": "plug_n_pay",
"card_type": "Visa",
"card_last_four": "1234",
"verification_required": false,
"created_at": "2026-04-15T14:30:00.000Z",
"metadata": {
"customer_name": "John Doe",
"authorization_code": "AUTH123",
"gateway_reference": "2026041514300000001"
}
}
}
verification_required is true when the transaction is a card-verification micro-charge awaiting amount confirmation. It is false (or omitted) for completed payments.
For security purposes, new cards must be verified before they can be used for payments. The verification process involves charging a small random amount to the card, which the customer must confirm.
The verification amount depends on the merchant's PlugNPay processing currency:
The customer can submit either the exact amount charged or the approximate equivalent in the other currency (to account for bank FX conversions on international card statements).
402 status and VERIFICATION_INITIATED error codeManually initiates the card verification process.
Endpoint: POST /cards/verification/initiate
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
card_number |
string | Yes | Full card number (16 digits) |
card_expiry_month |
string | Yes | Card expiry month (e.g., "12") |
card_expiry_year |
string | Yes | Card expiry year (e.g., "2025") |
card_cvv |
string | Yes | Card CVV/CVC code (3-4 digits) |
card_holder_name |
string | Yes | Name on the card |
customer_name |
string | No | Name of the customer |
customer_email |
string | No | Email of the customer |
currency |
string | No | Currency for the verification micro-charge ("BZD" or "USD"). Must be enabled for your gateway configuration. Defaults to your first enabled currency. |
Example Request:
curl -X POST https://api.openpay.bz/v1/cards/verification/initiate \
-H "x-api-key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"card_number": "4541250000000001",
"card_expiry_month": "12",
"card_expiry_year": "2025",
"card_cvv": "123",
"card_holder_name": "John Doe",
"customer_name": "John Doe",
"customer_email": "john@example.com"
}'
Success Response (200 OK):
{
"success": true,
"verification_required": true,
"message": "Verification initiated. A small random amount has been charged to your card."
}
Response — verification not required (merchant policy):
Some merchants are configured to skip card verification for all cards or for Belize-issued (local) cards. When the card is exempt:
{
"success": true,
"verification_required": false,
"message": "Card verification is not required for this merchant."
}
No charge is made. Proceed directly to payment. When verification IS
required, the response includes "verification_required": true alongside
the existing fields.
Error Response — currency not enabled (400 Bad Request):
{
"success": false,
"error": "currency_not_enabled",
"message": "USD is not enabled for this merchant — no USD gateway configuration"
}
Completes the card verification by submitting the verification amount.
Endpoint: POST /cards/verification/complete
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
card_number |
string | Yes | Full card number (16 digits) |
card_expiry_month |
string | Yes | Card expiry month (e.g., "12") |
card_expiry_year |
string | Yes | Card expiry year (e.g., "2025") |
amount |
number | Yes | The verification amount from the bank statement (e.g., 3.42 for BZD, 1.71 for USD) |
Example Request:
curl -X POST https://api.openpay.bz/v1/cards/verification/complete \
-H "x-api-key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"card_number": "4541250000000001",
"card_expiry_month": "12",
"card_expiry_year": "2025",
"amount": 3.42
}'
Success Response (200 OK):
{
"success": true,
"message": "Card verified successfully."
}
Error Response - Incorrect Amount (400 Bad Request):
{
"success": false,
"message": "Incorrect amount. Please try again.",
"remaining_attempts": 1
}
Error Response - Card Blocked (400 Bad Request):
After 2 failed verification attempts, the card is blocked:
{
"success": false,
"message": "Too many failed attempts. Card blocked.",
"remaining_attempts": 0
}
The API uses standard HTTP status codes to indicate the success or failure of requests.
| Status Code | Description |
|---|---|
200 |
OK - Request succeeded |
201 |
Created - Resource created successfully |
400 |
Bad Request - Invalid parameters or missing fields |
401 |
Unauthorized - Invalid or missing API Key |
402 |
Payment Required - Card verification required |
403 |
Forbidden - Access denied to the resource |
404 |
Not Found - Resource does not exist |
429 |
Too Many Requests - Abuse-detection block (card/IP/merchant velocity) |
500 |
Internal Server Error - Something went wrong on our end |
Error Response Body:
{
"success": false,
"message": "Description of the error"
}
Merchants with status test or sandbox can use all API endpoints normally, but no real charges are processed. All gateway calls are simulated and transactions are tagged with test_mode: true in their metadata.
Don't have an OpenPay account yet? Request sandbox access at www.openpay.bz — no paperwork required. You'll receive an invitation email; registering gives you the full merchant dashboard and API (your own API keys, payment links, hosted checkout) with every payment simulated. Sandbox accounts:
"test_mode": true at the top level when the merchant is in test mode.test to active).Use specific card number suffixes to trigger different gateway responses:
| Card number suffix | Simulated result |
|---|---|
Default (e.g., 4111111111111111) |
Approved |
*1000 (e.g., 4111111111111000) |
Insufficient Funds |
*2000 (e.g., 4111111111112000) |
Declined |
*3000 (e.g., 4111111111113000) |
CVV Mismatch |
*4000 (e.g., 4111111111114000) |
Expired Card |
*5000 (e.g., 4111111111115000) |
Processing Error |
{
"success": true,
"test_mode": true,
"data": {
"transaction_id": "abc-123-...",
"status": "completed",
"message": "Payment processed successfully",
"gateway_reference": "TEST-1713200000000",
"response_code": "APPROVED"
}
}
gateway_reference values prefixed with TEST-. These are not valid PlugNPay or BeaconPay references.test_mode flag is only present when the merchant is in test mode. Production responses do not include this field.test in the admin portal. To go live, the admin clicks "Activate Merchant" which wipes all test data and transitions to active.