Skip to main content

Webhooks

StablePay sends webhooks to notify you of events in real-time.

Setup

  1. Configure your webhook URL in the Dashboard
  2. Set a webhook secret for signature verification
  3. Ensure your endpoint returns 200 OK quickly

Webhook Format

All webhooks follow this structure:
{
  "event": "transaction.deposit_detected",
  "timestamp": "2025-01-05T10:30:00.000Z",
  "data": {
    // Event-specific payload
  }
}

Event Types

User Events

EventTrigger
user.createdUser created via API
user.kyc_updatedKYC status changed

Transaction Events

EventTrigger
transaction.createdTransaction created
transaction.deposit_detectedDeposit seen on blockchain
transaction.deposit_confirmedRequired confirmations met
transaction.payout_initiatedBank transfer started
transaction.payout_completedPayout successful
transaction.payout_failedPayout failed

Signature Verification

Verify webhook signatures to ensure authenticity:
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  // Parse: t=timestamp,v1=signature
  const [timestampPart, signaturePart] = signature.split(',');
  const timestamp = timestampPart.replace('t=', '');
  const receivedSig = signaturePart.replace('v1=', '');

  // Check timestamp (5 min tolerance)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    throw new Error('Webhook timestamp too old');
  }

  // Compute expected signature
  const message = `${timestamp}.${payload}`;
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex');

  // Compare
  if (!crypto.timingSafeEqual(
    Buffer.from(expectedSig),
    Buffer.from(receivedSig)
  )) {
    throw new Error('Invalid webhook signature');
  }

  return true;
}

// Express example
app.post('/webhooks/stablepay', (req, res) => {
  const signature = req.headers['x-stablepay-signature'];
  const payload = JSON.stringify(req.body);

  try {
    verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET);

    // Process webhook
    const { event, data } = req.body;
    handleWebhookEvent(event, data);

    res.status(200).json({ received: true });
  } catch (error) {
    res.status(401).json({ error: error.message });
  }
});

Retry Policy

If your endpoint doesn’t return 200-299:
AttemptDelay
1Immediate
21 second
32 seconds
44 seconds
58 seconds
After 5 failures, the webhook is marked as failed.

Manual Retry

Retry failed webhooks via API:
curl -X POST https://api.stablepay.global/v2/webhooks/{eventId}/retry \
  -H "Authorization: Bearer YOUR_API_KEY"

Best Practices

Return 200 immediately, then process async. We timeout after 30 seconds.
Webhooks may be delivered multiple times. Use eventId for deduplication.
Always verify webhook signatures in production.
Log webhook payloads for debugging and audit trails.

Testing Webhooks

Use the Dashboard to send test webhooks to your endpoint.