Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/own-pay/OwnPay-Documentation/llms.txt

Use this file to discover all available pages before exploring further.

These examples cover the most common OwnPay integration tasks using the Merchant REST API (/api/v1/*). Every request must be made from your server — never from a browser — because API keys must remain private. All code uses real OwnPay API contracts; substitute your brand’s base URL (https://pay.yourbrand.com) and a valid write-scoped API key.

Create a Payment and Redirect to Checkout

Call POST /api/v1/payments to create a Payment Intent and receive a checkout_url. Redirect the customer to that URL to complete payment.
<?php
$baseUrl = 'https://pay.yourbrand.com/api/v1';
$apiKey  = 'op.xxxxx.xxxxxx'; // write-scoped key

$payload = [
    'amount'         => '500.00',
    'currency'       => 'BDT',
    'callback_url'   => 'https://my-store.com/webhooks/ownpay',
    'redirect_url'   => 'https://my-store.com/checkout/success',
    'cancel_url'     => 'https://my-store.com/checkout/cancel',
    'customer_email' => 'customer@example.com',
    'customer_name'  => 'John Doe',
    'reference'      => 'ORDER-10024',
];

$ch = curl_init("$baseUrl/payments");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
    'Content-Type: application/json',
    'Accept: application/json',
]);

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$result = json_decode($response, true);

if ($httpCode === 201 && ($result['success'] ?? false)) {
    $checkoutUrl = $result['data']['checkout_url'];
    header("Location: $checkoutUrl");
    exit;
} else {
    echo 'Error creating payment: ' . ($result['error'] ?? 'Unknown error');
}
The checkout_url contains only a secure token — the payment amount and gateway details are never embedded in the URL. Do not construct checkout URLs manually.

Verify a Webhook Signature

Always verify webhook payloads using HMAC-SHA256 before acting on them. Use the raw request body — never decode and re-encode JSON before verifying.
<?php
// Your brand's Webhook Secret from Admin → Developer Hub → Webhooks
$webhookSecret = 'whsec_xxxxxxxxxxxxxxxxxxxx';

$rawPayload      = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_X_OWNPAY_SIGNATURE'] ?? '';

$expectedSignature = hash_hmac('sha256', $rawPayload, $webhookSecret);

// Timing-attack-safe comparison
if (!hash_equals($expectedSignature, $signatureHeader)) {
    http_response_code(401);
    exit('Invalid signature');
}

$event = json_decode($rawPayload, true);

if ($event['event'] === 'payment.transaction.completed') {
    $transaction = $event['data'];
    $orderRef    = $transaction['reference'];
    $amount      = $transaction['amount'];
    $currency    = $transaction['currency'];

    // Fulfill the order — reference is the value you passed at creation
    // fulfillOrder($orderRef, $amount, $currency);
}

http_response_code(200);
echo 'OK';
Never verify signatures by decoding the JSON and re-encoding it. Re-encoding changes whitespace and will break the HMAC comparison. Always pass the raw request body bytes to hash_hmac.

Poll Payment Status Until Completed

Use this pattern when a customer returns to your success page and you need to confirm the payment before fulfilling an order — as a complement to webhooks, not a replacement.
PHP
<?php
$baseUrl   = 'https://pay.yourbrand.com/api/v1';
$apiKey    = 'op.xxxxx.xxxxxx';
$paymentId = 'a810b445-564a-4e20-80a5-f1261d7b328a'; // UUID from payment creation

$maxAttempts = 12;  // poll for up to 60 seconds
$interval    = 5;   // seconds between attempts

for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
    $ch = curl_init("$baseUrl/payments/$paymentId");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: Bearer ' . $apiKey,
        'Accept: application/json',
    ]);

    $result   = json_decode(curl_exec($ch), true);
    curl_close($ch);

    if ($result['success'] ?? false) {
        $status = $result['data']['status'];

        if ($status === 'completed') {
            echo 'Payment completed. TRX ID: ' . $result['data']['trx_id'];
            // fulfillOrder($result['data']);
            break;
        }

        if (in_array($status, ['failed', 'cancelled', 'expired'], true)) {
            echo 'Payment ended with status: ' . $status;
            break;
        }
    }

    if ($attempt < $maxAttempts - 1) {
        sleep($interval);
    }
}
For real-time status updates on the checkout page itself, use the built-in GET /checkout/{token}/status AJAX endpoint rather than calling the API directly from the browser.

List Recent Transactions with Filters

Retrieve a paginated, filtered list of transactions using GET /api/v1/transactions. All filter parameters are optional.
PHP
<?php
$baseUrl = 'https://pay.yourbrand.com/api/v1';
$apiKey  = 'op.xxxxx.xxxxxx'; // read-scoped key is sufficient

$filters = http_build_query([
    'status'   => 'completed',
    'gateway'  => 'stripe',
    'from'     => '2026-01-01',
    'to'       => '2026-01-31',
    'per_page' => 25,
    'page'     => 1,
]);

$ch = curl_init("$baseUrl/transactions?$filters");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
    'Accept: application/json',
]);

$result = json_decode(curl_exec($ch), true);
curl_close($ch);

if ($result['success'] ?? false) {
    foreach ($result['data'] as $txn) {
        printf(
            "[%s] %s %s %s via %s\n",
            $txn['trx_id'],
            $txn['amount'],
            $txn['currency'],
            $txn['status'],
            $txn['gateway'],
        );
    }

    $meta = $result['meta'];
    echo "Page {$meta['page']} of {$meta['total_pages']} ({$meta['total']} total)\n";
}

Issue a Partial Refund

Call POST /api/v1/refunds to issue a partial or full refund. The refund amount cannot exceed the original transaction’s un-refunded balance.
PHP
<?php
$baseUrl = 'https://pay.yourbrand.com/api/v1';
$apiKey  = 'op.xxxxx.xxxxxx'; // write-scoped key required

$payload = [
    'trx_id' => 'OP-000012345',
    'amount' => '150.00',   // partial refund — must be ≤ remaining refundable amount
    'reason' => 'Customer requested partial refund for damaged item',
];

$ch = curl_init("$baseUrl/refunds");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
    'Content-Type: application/json',
    'Accept: application/json',
]);

$result   = json_decode(curl_exec($ch), true);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($httpCode === 201 && ($result['success'] ?? false)) {
    echo 'Refund issued. UUID: ' . $result['data']['uuid'];
} else {
    echo 'Refund failed: ' . ($result['error'] ?? 'Unknown error');
}
Refunds are routed to the same gateway that processed the original payment. The refund amount is posted as a balanced debit/credit pair in the double-entry ledger automatically.

Create a Customer

Use POST /api/v1/customers to create a customer record. Customer PII (name, email, phone) is encrypted at rest using AES-256-GCM. Email must be unique per brand — creating a customer with a duplicate email returns 409 Conflict.
PHP
<?php
$baseUrl = 'https://pay.yourbrand.com/api/v1';
$apiKey  = 'op.xxxxx.xxxxxx'; // write-scoped key required

$payload = [
    'name'  => 'Jane Smith',
    'email' => 'jane.smith@example.com',
    'phone' => '+8801700000000',
];

$ch = curl_init("$baseUrl/customers");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $apiKey,
    'Content-Type: application/json',
    'Accept: application/json',
]);

$result   = json_decode(curl_exec($ch), true);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if (in_array($httpCode, [200, 201], true) && ($result['success'] ?? false)) {
    $customer = $result['data'];
    echo 'Customer ID: ' . $customer['id'] . PHP_EOL;
    echo 'UUID: '        . $customer['uuid'] . PHP_EOL;
}

Plugin: Apply a Custom Fee Filter

If you are developing an Addon Plugin, use EventManager filters to modify the payment amount before processing. Always use bcmath for all financial arithmetic.
PHP
<?php
use OwnPay\Event\EventManager;

EventManager::addFilter('payment.amount', function (string $amount, array $payment): string {
    // Add a 2% convenience fee for credit card payments
    if (($payment['gateway'] ?? '') === 'stripe-card') {
        // bcmath — never floats for money
        return bcmul($amount, '1.02', 2);
    }
    return $amount;
}, 10, 2);

Plugin: React to Completed Payments

Use doAction callbacks to trigger side effects when a transaction succeeds.
PHP
<?php
use OwnPay\Event\EventManager;

EventManager::addAction('payment.completed', function (array $transaction): void {
    $email    = $transaction['customer']['email'] ?? null;
    $amount   = $transaction['amount'];
    $currency = $transaction['currency'];

    if ($email) {
        // Send a custom receipt or trigger a fulfillment workflow
        mail(
            $email,
            'Payment Received',
            "Thank you for your payment of {$amount} {$currency}.",
        );
    }
}, 10, 1);

Build docs developers (and LLMs) love