OpenPay API Reference

Developer documentation for the OpenPay payment processing API

OpenPay Public API Documentation

The OpenPay Public API allows merchants to programmatically create payment links, retrieve transaction details, and integrate OpenPay directly into their applications.

Authentication

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_...

Base URL

https://api.openpay.bz/v1

Paylinks

Paylinks are secure, shareable URLs that allow customers to make payments.

Create a Paylink

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.

Retrieve a Paylink

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

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.

How it works

  1. Register an HTTPS endpoint using POST /v1/webhooks.
  2. OpenPay delivers a signed POST request to that URL whenever a matching event fires.
  3. Your endpoint verifies the signature (see Signature Verification) and returns any 2xx status to acknowledge receipt.
  4. If delivery fails (non-2xx or timeout), OpenPay retries with exponential backoff (see Retry Policy).

Event catalog

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.paid

Emitted 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.completed

Emitted 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.failed

Emitted 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.completed

Emitted 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.partial

Emitted 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"
  }
}

Idempotency

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.


Signature verification

Every delivery request carries an X-OpenPay-Signature header. Verify it to confirm the payload came from OpenPay and has not been tampered with.

Header format

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.

Signature construction

signed_payload = "<t>.<raw_request_body>"
v1 = HMAC_SHA256(signing_secret, signed_payload)

Replay protection

Reject any delivery whose timestamp t is more than 5 minutes in the past (or future). This prevents replay attacks.

Node.js verification example

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()), not express.json().

Python verification example

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

Request headers sent with every delivery

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

Retry policy

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.


Management API

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>.

Create a webhook endpoint

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.

List webhook endpoints

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 a webhook endpoint

GET /v1/webhooks/:id

curl https://api.openpay.bz/v1/webhooks/wh_01234567-89ab-cdef-0123-456789abcdef \
  -H "x-api-key: sk_live_..."

Update a webhook endpoint

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 a webhook endpoint

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_..."

Rotate the signing secret

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"
  }
}

Send a test event

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.

List deliveries

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"
    }
  ]
}

Direct Payments

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).

Process a Payment

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.


Transactions

List Transactions

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
  }
}

Get Transaction

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.


Card Verification

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).

Verification Flow

  1. Customer attempts a payment with a new card
  2. API responds with 402 status and VERIFICATION_INITIATED error code
  3. A small random amount is charged to the card in the merchant's processing currency
  4. Customer checks their bank statement for the exact amount
  5. Customer submits the verification amount via the completion endpoint
  6. Upon successful verification, the card can be used for future payments

Initiate Card Verification

Manually 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"
}

Complete Card Verification

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
}

Errors

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"
}

Test Mode & the Sandbox

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.

Getting a sandbox account

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:

How it works

Controlling simulated outcomes

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

Example test-mode response

{
  "success": true,
  "test_mode": true,
  "data": {
    "transaction_id": "abc-123-...",
    "status": "completed",
    "message": "Payment processed successfully",
    "gateway_reference": "TEST-1713200000000",
    "response_code": "APPROVED"
  }
}

Notes