Webhook Documentation

Learn how to receive webhook notifications from Payment Hub

← Back to Payment Hub

Overview

Payment Hub sends webhook notifications when invoices are paid or expire. Webhooks are configured at the store level and use Ed25519 signatures for security.

Three types of POST requests are sent to your webhook URL:

Getting Started: Configuring Your Store

To receive webhooks, you must first create a Store and configure its webhook settings via the Paytaca Wallet.

  1. Open the Paytaca Wallet app.
  2. Navigate to the Payment Hub App within the wallet.
  3. Click Create Store and fill out your store's details.
  4. Enter your Webhook URL in the store configuration.
  5. Save the configuration and click Refresh Keys to generate your secure Ed25519 keypair.
  6. Copy your Public Key (PEM format). You will use this key on your server to verify the authenticity of all incoming webhooks.

Note: Payment Hub uses highly secure Ed25519 public-key cryptography to sign webhooks. The payload must be parsed as a compact, sorted JSON string before verifying with the Public Key.

Creating Invoices with Store

When creating an invoice, include the store_id to associate it with your store's webhook configuration.

Create Invoice with Store:

{
  "recipients": [
    {
      "amount": 5,
      "address": "bitcoincash:qpgue6yr2ha2rymjhjg384lvcwpk5hmqvyjem58nlr",
      "description": "Payment for goods or services"
    }
  ],
  "currency": "USD",
  "memo": "Test Order #101",
  "store_id": "550e8400-e29b-41d4-a716-446655440000",
  "redirect_url": "https://example.com/redirect"
}

When you include a store_id in your invoice creation request, Payment Hub will automatically use the store's webhook URL and public key for all webhook notifications related to that invoice.

Webhook Security (Ed25519)

Payment Hub uses Ed25519 public-key cryptography to ensure webhook authenticity and prevent tampering.

Signature Header

Every webhook request includes a base64-encoded signature header:

X-Webhook-Signature: Base64EncodedSignatureHere...

Verification Examples

Python

from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
from cryptography.exceptions import InvalidSignature
import base64
import json

def verify_webhook_signature(payload_dict, signature_b64, public_key_pem):
    # Convert payload to compact, sorted JSON string to match backend exactly
    payload_str = json.dumps(payload_dict, separators=(',', ':'), sort_keys=True)
    
    # Decode base64 signature
    signature_bytes = base64.b64decode(signature_b64)
    
    # Load Ed25519 public key
    public_key = serialization.load_pem_public_key(public_key_pem.encode('utf-8\))
    
    try:
        public_key.verify(signature_bytes, payload_str.encode('utf-8\))
        return True
    except InvalidSignature:
        return False

# Usage
if verify_webhook_signature(request.json(), request.headers.get('X-Webhook-Signature\), "YOUR_PUBLIC_KEY"):
    print("Signature verified!")

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(payloadDict, signatureB64, publicKeyPem) {
    // Stringify with sorted keys and no spaces
    const payloadStr = JSON.stringify(payloadDict, Object.keys(payloadDict).sort());
    
    const signatureBytes = Buffer.from(signatureB64, 'base64');
    
    return crypto.verify(
        null, 
        Buffer.from(payloadStr), 
        publicKeyPem, 
        signatureBytes
    );
}

// Usage
if (verifyWebhookSignature(req.body, req.headers['x-webhook-signature'], "YOUR_PUBLIC_KEY")) {
    console.log("Signature verified!");
}

PHP

<?php
function verifyWebhookSignature($payload_array, $signature_b64, $public_key_pem) {
    // Sort keys recursively if needed, but for flat payload ksort is enough
    ksort($payload_array);
    $payload_str = json_encode($payload_array, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    
    $signature_bytes = base64_decode($signature_b64);
    $public_key_res = openssl_pkey_get_public($public_key_pem);
    $public_key_bytes = openssl_pkey_get_details($public_key_res)['key'];
    
    return sodium_crypto_sign_verify_detached($signature_bytes, $payload_str, $public_key_bytes);
}

// Usage
if (verifyWebhookSignature($_POST, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'], "YOUR_PUBLIC_KEY")) {
    echo "Signature verified!";
}

When Invoice is Paid

A POST request is sent to your webhook URL when an invoice payment is completed successfully.

Sample Request:

POST /webhook HTTP/1.1
Host: example.com
Content-Type: application/json
X-Webhook-Signature: Base64EncodedSignatureHere...

{
  "event": "invoice.paid",
  "invoice_id": "e876ed40-3c2e-4019-9b33-dfe3f8027905",
  "store_id": "a1b2c3d4-...",
  "status": "PAID",
  "timestamp": "2026-06-24T10:05:32+00:00",
  "payment_txid": "199cd87d160a1c497bc8ea54f278fa1b8a34e685e8350e0c7cfa4303da82ffc9",
  "date_paid": "2026-06-24T10:05:32+00:00"
}

Field Descriptions:

event: Event type (e.g., invoice.paid)
invoice_id: Unique invoice UUID
store_id: Associated store UUID
status: Current status (e.g., PAID)
timestamp: ISO timestamp of the event
payment_txid: Bitcoin Cash transaction hash
date_paid: ISO timestamp when payment was received

When Invoice Expires

A POST request is sent to your webhook URL when an invoice expires without being paid.

Sample Request:

POST /webhook HTTP/1.1
Host: example.com
Content-Type: application/json
X-Webhook-Signature: Base64EncodedSignatureHere...

{
  "event": "invoice.expired",
  "invoice_id": "e876ed40-3c2e-4019-9b33-dfe3f8027905",
  "store_id": "a1b2c3d4-...",
  "status": "EXPIRED",
  "timestamp": "2026-06-24T10:05:32+00:00"
}

Field Descriptions:

invoice_id: Unique invoice identifier
status: Always "expired" for expired invoices

When Invoice is Cancelled

A POST request is sent to your webhook URL when an invoice is manually cancelled by the user.

Sample Request:

POST /webhook HTTP/1.1
Host: example.com
Content-Type: application/json
X-Webhook-Signature: Base64EncodedSignatureHere...

{
  "invoice_id": "e876ed403c2e40199b33dfe3f8027905",
  "status": "cancelled"
}

Field Descriptions:

invoice_id: Unique invoice identifier
status: Always "cancelled" for cancelled invoices

Implementation Notes

Webhook URL Requirements

  • Must be a valid HTTPS URL
  • Should respond with HTTP 200 status code
  • Should handle the webhook within 30 seconds
  • Should verify the Ed25519 signature before processing

Security Best Practices

  • Always verify the webhook signature before processing
  • Store your webhook public key securely
  • Use HTTPS for all webhook endpoints
  • Implement idempotency to handle duplicate webhooks
  • Log webhook events for debugging and auditing

Error Handling

  • Return HTTP 200 for successful webhook processing
  • Return HTTP 4xx for invalid requests (malformed payload, invalid signature)
  • Return HTTP 5xx for server errors (temporary issues)
  • Payment Hub will retry failed webhooks up to 7 times

Testing Webhooks

You can test your webhook implementation using tools like ngrok or webhook.site:

Using webhook.site:

  1. Go to webhook.site
  2. Copy your unique webhook URL
  3. Use this URL when creating your store
  4. Create an invoice and watch for webhook notifications

Using ngrok:

  1. Install ngrok: npm install -g ngrok
  2. Start your local server: python manage.py runserver
  3. Expose your local server: ngrok http 8000
  4. Use the ngrok URL as your webhook URL

Need Help?

If you need assistance with webhook implementation or have questions about the API, please contact our support team.