Skip to main content

Webhooks

Webhooks let your application receive real-time notifications when events occur in Open Pay. Every webhook is signed with ED25519 so you can verify its authenticity.

Configure Your Webhook Endpoint

Register your webhook URL using the API:
curl -X POST https://olp-api.nipuntheekshana.com/v1/webhooks/configure \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/openpay",
    "events": [
      "payment.completed",
      "payment.failed",
      "payment.expired",
      "subscription.renewed",
      "subscription.cancelled"
    ]
  }'
Your endpoint must return a 2xx status code within 10 seconds. Any other response is treated as a failure and triggers a retry.

Event Types

EventDescription
payment.completedPayment confirmed on-chain and settled
payment.failedTransaction reverted or rejected
payment.expiredPayment TTL exceeded without completion
subscription.renewedRecurring subscription payment collected
subscription.cancelledSubscription cancelled by merchant or customer

Webhook Payload

Every webhook delivery includes these headers:
HeaderDescription
X-OpenPay-SignatureED25519 signature of the payload (base64-encoded)
X-OpenPay-TimestampUnix timestamp of when the webhook was sent
Content-Typeapplication/json
Example payload:
{
  "id": "evt_xyz789",
  "event": "payment.completed",
  "created_at": "2026-03-26T13:02:15Z",
  "data": {
    "payment_id": "pay_abc123",
    "status": "paid",
    "amount": "25.00",
    "currency": "USD",
    "token": "USDT",
    "tx_hash": "0xabc123...",
    "metadata": {
      "order_id": "1042"
    }
  }
}

Signature Verification

1

Get the Public Key

Retrieve Open Pay’s ED25519 public key for verification:
curl https://olp-api.nipuntheekshana.com/v1/webhooks/public-key \
  -H "Authorization: Bearer sk_live_..."
{
  "public_key": "MCowBQYDK2VwAyEA...",
  "algorithm": "ED25519",
  "format": "base64"
}
Cache the public key in your application. It only rotates during key rotation events, which are announced in advance.
2

Construct the Signed Message

The signed message is the concatenation of the timestamp and the raw request body:
signed_message = timestamp + "." + raw_body
3

Verify the Signature

Verify the ED25519 signature against the signed message using the public key.
4

Validate the Timestamp

Reject any webhook where the timestamp is more than 5 minutes old to prevent replay attacks.

Code Examples

import { createServer } from 'http';
import { verify } from '@noble/ed25519';

const PUBLIC_KEY = process.env.OPENPAY_WEBHOOK_PUBLIC_KEY!;
const TOLERANCE_SECONDS = 300; // 5 minutes

async function verifyWebhook(
  signature: string,
  timestamp: string,
  body: string
): Promise<boolean> {
  // 1. Check timestamp freshness
  const ts = parseInt(timestamp, 10);
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - ts) > TOLERANCE_SECONDS) {
    console.error('Webhook timestamp too old');
    return false;
  }

  // 2. Construct signed message
  const message = new TextEncoder().encode(`${timestamp}.${body}`);

  // 3. Verify ED25519 signature
  const sig = Buffer.from(signature, 'base64');
  const pubKey = Buffer.from(PUBLIC_KEY, 'base64');

  return await verify(sig, message, pubKey);
}

// Express example
import express from 'express';
const app = express();

app.post(
  '/webhooks/openpay',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.headers['x-openpay-signature'] as string;
    const timestamp = req.headers['x-openpay-timestamp'] as string;
    const body = req.body.toString();

    const isValid = await verifyWebhook(signature, timestamp, body);
    if (!isValid) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(body);

    switch (event.event) {
      case 'payment.completed':
        await fulfillOrder(event.data.payment_id, event.data.metadata);
        break;
      case 'payment.failed':
        await handleFailedPayment(event.data.payment_id);
        break;
      case 'payment.expired':
        await handleExpiredPayment(event.data.payment_id);
        break;
      case 'subscription.renewed':
        await extendSubscription(event.data);
        break;
      case 'subscription.cancelled':
        await cancelSubscription(event.data);
        break;
    }

    res.status(200).json({ received: true });
  }
);

Retry Policy

If your endpoint does not return a 2xx response, Open Pay retries with exponential backoff:
RetryDelayCumulative Time
1st30 seconds30s
2nd1 minute1m 30s
3rd5 minutes6m 30s
4th15 minutes21m 30s
5th1 hour1h 21m 30s
After 5 failed retries, the webhook delivery is marked as failed. You can view failed deliveries and manually retry them from the Merchant Portal or via GET /v1/webhooks/deliveries.

Testing Webhooks

Use the test endpoint to send a sample webhook to your configured URL:
curl -X POST https://olp-api.nipuntheekshana.com/v1/webhooks/test \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "event": "payment.completed"
  }'
This sends a test payload with dummy data to your webhook URL, signed with the same key used in production. Use it to validate your signature verification logic.
During development, use a tool like ngrok to expose your local server and receive webhooks.

Best Practices

Verify Every Signature

Never process a webhook without verifying the ED25519 signature and checking the timestamp window.

Respond Quickly

Return 200 OK immediately and process the event asynchronously. Webhook delivery times out after 10 seconds.

Handle Duplicates

Use the id field to deduplicate events. The same event may be delivered more than once during retries.

Log Deliveries

Store webhook payloads for debugging. You can also review delivery history at GET /v1/webhooks/deliveries.