Webhooks

Webhooks allow you to receive real-time notifications when events occur in your Rebel Pay account, such as when a payment confirms.

Event Types

  • charge.created — A new charge was created
  • charge.pending — Payment detected in mempool, awaiting confirmations
  • charge.confirmed — Payment confirmed on the blockchain (at your configured minimum: 3, 5, or 10 confirmations)
  • charge.expired — Charge expired without payment (1 hour default)
  • charge.underpaid — Payment received but amount is less than required
  • payout.sent — XMR payout swept to merchant's payout wallet
  • swap.auto_created — Auto-convert swap initiated on charge confirmation (see Auto-Convert)

Webhook Payload

All webhooks are sent as HTTP POST requests with JSON body:

POST https://yoursite.com/webhooks/rebelpay
Content-Type: application/json
X-REBELPAY-SIGNATURE: <hmac-sha256-signature>

{
  "event": "charge.confirmed",
  "data": {
    "id": "ch_abc123def456",
    "amount": 25.00,
    "currency": "USD",
    "amount_xmr": 0.071428,
    "subaddress": "84Hv16y6x7BTie3ib5Sx...",
    "status": "confirmed",
    "tx_hash": "7d2e4f8a1b3c5d6e...",
    "confirmations": 10,  // actual count at confirmation time
    "metadata": {
      "order_id": "12345",
      "buyer_fields": "email,name",
      "buyer_info": {
        "email": "customer@example.com",
        "name": "Jane Smith"
      }
    },
    "created_at": "2026-02-15T10:00:00.000Z",
    "confirmed_at": "2026-02-15T10:20:00.000Z"
  },
  "timestamp": "2026-02-15T10:20:00.000Z"
}

Signature Verification

Every webhook includes an X-REBELPAY-SIGNATURE header containing an HMAC-SHA256 signature of the request body.

Always verify signatures to ensure webhooks are genuine:

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  // Use timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

// Express handler
app.post('/webhooks/rebelpay', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-rebelpay-signature'];
  const payload = req.body.toString();

  if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(payload);

  switch (event.event) {
    case 'charge.confirmed':
      // Fulfill the order
      fulfillOrder(event.data.metadata.order_id);
      break;
    case 'charge.expired':
      // Handle expiration
      cancelOrder(event.data.metadata.order_id);
      break;
  }

  res.status(200).send('OK');
});

Python

import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

# Flask handler
@app.route('/webhooks/rebelpay', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-REBELPAY-SIGNATURE')
    payload = request.get_data()

    if not verify_signature(payload, signature, WEBHOOK_SECRET):
        return 'Invalid signature', 401

    event = request.get_json()

    if event['event'] == 'charge.confirmed':
        fulfill_order(event['data']['metadata']['order_id'])

    return 'OK', 200

PHP

<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_REBELPAY_SIGNATURE'];
$secret = 'your_webhook_secret';

$expected = hash_hmac('sha256', $payload, $secret);

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

$event = json_decode($payload, true);

if ($event['event'] === 'charge.confirmed') {
    fulfillOrder($event['data']['metadata']['order_id']);
}

echo 'OK';

Webhook Secret

When you create a webhook, you receive a secret key. Store this securely - it's only shown once. If you lose it, delete and recreate the webhook.

Retry Policy

If your endpoint returns a non-2xx status code or the connection fails, we'll retry with exponential backoff:

  • 1st retry: 5 seconds
  • 2nd retry: 10 seconds

After 3 total attempts (initial + 2 retries), the delivery is marked as failed. All delivery attempts are logged in the webhook delivery log on your dashboard.

Security Best Practices

  • Always verify signatures: Never process a webhook without validating the HMAC signature. Attackers can forge webhook payloads to trigger false fulfillments.
  • Use timing-safe comparison: Use crypto.timingSafeEqual (Node.js), hmac.compare_digest (Python), or hash_equals (PHP) to prevent timing attacks.
  • Validate charge status independently: After receiving a webhook, query GET /api/charges/:id to confirm the status server-side before fulfilling orders.
  • Handle duplicates: Use the charge ID as an idempotency key. You may receive the same event more than once.
  • Respond quickly: Return 200 within 10 seconds. Queue long-running tasks.
  • Use HTTPS: Webhook URLs must use HTTPS in production.
  • Don't trust metadata blindly: Metadata is merchant-supplied. Validate it against your own records.
  • Buyer fields are buyer-supplied: Data in metadata.buyer_info comes from the customer. Treat it as untrusted input — validate and sanitize before use.

Testing Webhooks

Use services like webhook.site or ngrok to test webhooks during development.