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.

Webhooks are the backbone of a real-time OwnPay integration. Instead of polling the API to check payment status, OwnPay pushes an HTTP POST to your server the moment a payment event occurs — when a payment is confirmed, fails, is refunded, or when a payment link is paid. This guide covers everything from configuring your endpoint to writing resilient, idempotent handlers in PHP and Node.js.

How Webhooks Work

Customer pays → OwnPay processes → POST to your endpoint

                               Verify signature

                               Handle event type

                               Return HTTP 200 quickly
OwnPay considers delivery successful when your endpoint returns HTTP 2xx within 10 seconds. On failure or timeout, it retries with exponential backoff:
AttemptDelay After Previous Failure
21 minute
35 minutes
430 minutes
52 hours
6 (final)12 hours
After 6 failed attempts, delivery is abandoned and the event is marked as failed in the delivery log.

Configuring an Endpoint

1
Open the Webhook Settings
2
Log in to your OwnPay admin panel and navigate to Admin Panel → Developer Hub → Webhooks. Click Add Endpoint.
3
Enter Your Endpoint URL
4
Enter the full HTTPS URL of your webhook receiver:
5
https://your-store.com/webhooks/ownpay
6
OwnPay only delivers webhooks to https:// endpoints. For local development, use ngrok to expose a local port over a public HTTPS URL, then register that URL as your endpoint.
7
Select Events
8
Choose which events should trigger delivery to this endpoint. You can select individual events or choose All events to receive everything. See the Event Types table below for the full list.
9
Copy the Webhook Secret
10
After saving the endpoint, OwnPay generates a Webhook Signing Secret. Copy it immediately — it is shown only once and cannot be retrieved again.
11
# Store it as an environment variable
OWNPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx

Webhook Request Format

Every webhook delivery is an HTTP POST with the following headers and a JSON body: Headers:
Content-Type: application/json
X-OwnPay-Signature: sha256=<hmac_hex>
X-OwnPay-Timestamp: 1718000000
X-OwnPay-Event: payment.paid
Body — payment.completed example:
{
  "event": "payment.completed",
  "created_at": "2024-06-15T10:30:00Z",
  "data": {
    "id": "pi_01j0x8kz7m3wd4b9qfhec5rtg6",
    "trx_id": "OP-481029304",
    "gateway_trx_id": "A8K9D2J3S",
    "amount": "500.00",
    "currency": "BDT",
    "fee": "7.50",
    "status": "completed",
    "gateway": "bkash",
    "reference": "INV-10029",
    "description": "Order #1042",
    "customer": {
      "name": "Rahim Uddin",
      "email": "rahim@example.com"
    },
    "metadata": {
      "order_id": "1042"
    },
    "paid_at": "2024-06-15T10:29:58Z"
  }
}

Signature Verification

Never skip signature verification. Anyone on the internet can POST to your webhook URL. The HMAC signature is the only proof that a delivery originated from your OwnPay instance.
The signature algorithm:
HMAC-SHA256(webhookSecret, timestamp + "." + rawRequestBody)
The result is hex-encoded and prefixed with sha256= in the X-OwnPay-Signature header. The timestamp included in the computation is the Unix epoch value from the X-OwnPay-Timestamp header.
<?php

declare(strict_types=1);

function verifyOwnPaySignature(string $secret): array
{
    $signature = $_SERVER['HTTP_X_OWNPAY_SIGNATURE'] ?? '';
    $timestamp  = $_SERVER['HTTP_X_OWNPAY_TIMESTAMP'] ?? '';
    $rawBody    = file_get_contents('php://input') ?: '';

    // Replay protection: reject events older than 5 minutes
    if (abs(time() - (int) $timestamp) > 300) {
        http_response_code(400);
        exit('Timestamp too old');
    }

    $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);

    if (!hash_equals($expected, $signature)) {
        http_response_code(401);
        exit('Invalid signature');
    }

    $event = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);

    if (!is_array($event)) {
        http_response_code(400);
        exit('Invalid JSON');
    }

    return $event;
}

// --- Webhook endpoint ---
$secret = getenv('OWNPAY_WEBHOOK_SECRET') ?: throw new RuntimeException('Missing OWNPAY_WEBHOOK_SECRET');
$event  = verifyOwnPaySignature($secret);

match ($event['event']) {
    'payment.paid'     => handlePaymentPaid($event['data']),
    'payment.failed'   => handlePaymentFailed($event['data']),
    'payment.refunded' => handlePaymentRefunded($event['data']),
    default            => null, // safely ignore unknown event types
};

http_response_code(200);
echo 'OK';
Raw body is required. Read the body as raw bytes before any JSON parsing. In Express, use express.raw({ type: 'application/json' }) on the webhook route. In PHP, read via file_get_contents('php://input'). If the body is parsed first, the byte representation changes and signature verification fails.

Event Types

EventDescription
payment.pendingIntent created; customer has not paid yet
payment.processingGateway confirmed receipt; settlement in progress
payment.paidPayment fully confirmed and settled
payment.completedAlias for payment.paid used by some gateway integrations
payment.failedPayment attempt failed
payment.cancelledCustomer or merchant cancelled the intent
payment.refundedFull refund issued
payment.partial_refundedPartial refund issued
link.visitedA payment link was opened by a customer
link.paidA payment via payment link was completed
Subscribe to All events during development so you can inspect every payload shape. In production, subscribe only to the events your handler actually processes to reduce noise and unnecessary traffic.

Retry Behavior and Delivery Logs

Viewing Delivery History

Inspect the 50 most recent webhook delivery attempts — including the payload sent, the HTTP status received, retry count, and final status:
GET /api/v1/webhooks/deliveries
Authorization: Bearer op_live_xxx...
Response:
{
  "success": true,
  "data": [
    {
      "id": 1024,
      "transaction_id": 45,
      "event": "payment.completed",
      "url": "https://my-store.com/webhooks/ownpay",
      "payload": "{\"event\":\"payment.completed\",\"data\":{\"trx_id\":\"OP-481029304\",\"amount\":\"500.00\"}}",
      "response_status": 200,
      "response_body": "OK",
      "attempts": 1,
      "status": "success",
      "created_at": "2026-06-23T14:17:15Z"
    }
  ]
}
You can also browse the delivery log visually in OwnPay Admin → Developer Hub → Webhooks → Delivery Log, where you can replay failed events manually.

Testing Webhooks

Fire a Test Event via API

Send a sample payment.completed payload to your configured webhook endpoint to verify reachability and confirm your handler returns 200:
POST /api/v1/webhooks/tests
Authorization: Bearer op_live_xxx...
Response:
{
  "success": true,
  "data": {
    "status_code": 200,
    "response_time_ms": 245
  }
}

Expose a Local Server with ngrok

For active development, tunnel your local server to a public HTTPS URL:
ngrok http 8080
# → https://xxxx.ngrok-free.app
Register the ngrok URL as a webhook endpoint in OwnPay admin. All real-time events during testing will arrive at your local process, making it easy to step through your handler with a debugger.

Idempotency

Webhook delivery is at-least-once — in rare cases (network timeouts, server restarts before your handler returned 200), OwnPay may deliver the same event more than once. Make every handler idempotent:
function handlePaymentPaid(array $data): void
{
    $orderId = $data['metadata']['order_id'];

    // Guard: skip if already fulfilled
    $order = Order::find($orderId);
    if ($order === null || $order->status === 'fulfilled') {
        return;
    }

    $order->fulfill();
}

Best Practices

Return 200 Immediately

Acknowledge the webhook immediately, then process asynchronously in a background queue. Slow handlers cause retries and duplicate delivery.

Always Verify Signatures

Never process an event without verifying the HMAC-SHA256 signature. Skip this step and any bad actor can forge payment confirmations.

Verify Before Fulfilling

After receiving a payment.paid event, confirm status by calling GET /payments/{id} or GET /transactions/{trx_id} before fulfilling orders.

Handle Unknown Events

Your handler will receive new event types as OwnPay adds them. Default branches should return 200 silently rather than 400 or 500.
Webhook events include a metadata field that mirrors the metadata you passed when creating the payment intent. Use this to carry your internal order_id, user_id, or any other identifiers through the payment flow without additional lookups.

Next Steps

PHP Integration

Full PHP client for payment intents, payment links, and transactions.

Node.js Integration

Full TypeScript/Node.js client with type definitions and Express examples.

API Reference

Complete OpenAPI spec including all webhook payload schemas.

Developer Quickstart

Go from zero to a working integration in five minutes.

Build docs developers (and LLMs) love