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
$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
$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
$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
$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
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
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);