Learn how to receive webhook notifications from Payment Hub
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:
To receive webhooks, you must first create a Store and configure its webhook settings via the Paytaca Wallet.
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.
When creating an invoice, include the store_id to associate it with your store's webhook configuration.
{
"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.
Payment Hub uses Ed25519 public-key cryptography to ensure webhook authenticity and prevent tampering.
Every webhook request includes a base64-encoded signature header:
X-Webhook-Signature: Base64EncodedSignatureHere...
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!")
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
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!";
}
A POST request is sent to your webhook URL when an invoice payment is completed successfully.
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"
}
A POST request is sent to your webhook URL when an invoice expires without being paid.
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"
}
A POST request is sent to your webhook URL when an invoice is manually cancelled by the user.
POST /webhook HTTP/1.1
Host: example.com
Content-Type: application/json
X-Webhook-Signature: Base64EncodedSignatureHere...
{
"invoice_id": "e876ed403c2e40199b33dfe3f8027905",
"status": "cancelled"
}
You can test your webhook implementation using tools like ngrok or webhook.site:
npm install -g ngrokpython manage.py runserverngrok http 8000If you need assistance with webhook implementation or have questions about the API, please contact our support team.