SafePays API

Webhooks

Real-time notifications for invoice payment events

Overview

When creating an invoice, you can provide a webhook_url in the request body. This URL will receive a POST request when payment is confirmed and all fees are calculated.

How Webhooks Work

Provide Webhook URL

Include webhook_url in the request body when creating an invoice via POST /api/v2/invoice

Invoice Status Changes

A customer makes a payment or an invoice status changes.

SafePays Sends Notification

SafePays sends a POST request with the invoice data to your webhook URL.

Process the Event

Your server processes the webhook payload and updates your system accordingly.

Webhook Payload

When an invoice status changes, your webhook URL receives a POST request containing the full invoice data. Check the status field to determine if payment was successful.

Example Payload

{
  "invoice": {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "amount": 125.00,
    "currency": "USD",
    "email": "john.doe@example.com",
    "provider": "card",
    "status": "Paid",
    "created_on": "15 Jan, 2024",
    "paid_on": "15 Jan, 2024",
    "payment_link": "https://app.safepays.com/pay/660e8400-e29b-41d4-a716-446655440001",
    "items": [
      {
        "name": "Product A",
        "qty": 2,
        "price": 50.00
      }
    ],
    "due_date": "2024-12-31",
    "customer_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}

Invoice Status Values

StatusDescription
Invoice CreatedInvoice created, awaiting payment
PaidPayment received successfully
UnpaidInvoice is past due
FailedPayment attempt failed

Implementing a Webhook Endpoint

Your webhook endpoint should:

  1. Accept POST requests with a JSON body
  2. Process the invoice data based on the status field
  3. Return a 200 OK response quickly

Example Implementations

app.post('/webhook/safepays', express.json(), async (req, res) => {
  try {
    const { invoice } = req.body;

    switch (invoice.status) {
      case 'Paid':
        await handlePaymentSuccess(invoice);
        break;
      case 'Failed':
        await handlePaymentFailed(invoice);
        break;
    }

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).send('Webhook processing failed');
  }
});

async function handlePaymentSuccess(invoice) {
  await db.invoices.update({
    where: { external_id: invoice.id },
    data: {
      status: 'paid',
      paid_at: invoice.paid_on
    }
  });

  await sendPaymentConfirmation(invoice.email, invoice);
}

async function handlePaymentFailed(invoice) {
  await db.payment_failures.create({
    invoice_id: invoice.id,
    failed_at: new Date()
  });

  await sendPaymentFailedEmail(invoice.email, invoice);
}
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook/safepays', methods=['POST'])
def safepays_webhook():
    try:
        data = request.get_json()
        invoice = data.get('invoice')

        if invoice['status'] == 'Paid':
            handle_payment_success(invoice)
        elif invoice['status'] == 'Failed':
            handle_payment_failed(invoice)

        return jsonify({'received': True}), 200

    except Exception as e:
        print(f'Webhook error: {e}')
        return jsonify({'error': 'Webhook processing failed'}), 500

def handle_payment_success(invoice):
    db.execute(
        "UPDATE invoices SET status = 'paid', paid_at = %s WHERE external_id = %s",
        (invoice['paid_on'], invoice['id'])
    )
    send_payment_confirmation(invoice['email'], invoice)

def handle_payment_failed(invoice):
    db.execute(
        "INSERT INTO payment_failures (invoice_id, failed_at) VALUES (%s, NOW())",
        (invoice['id'],)
    )
    send_payment_failed_email(invoice['email'], invoice)
<?php
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);
$invoice = $data['invoice'] ?? [];

try {
    switch ($invoice['status'] ?? '') {
        case 'Paid':
            handlePaymentSuccess($invoice);
            break;
        case 'Failed':
            handlePaymentFailed($invoice);
            break;
    }

    http_response_code(200);
    echo json_encode(['received' => true]);

} catch (Exception $e) {
    error_log('Webhook error: ' . $e->getMessage());
    http_response_code(500);
    echo json_encode(['error' => 'Webhook processing failed']);
}

function handlePaymentSuccess($invoice) {
    global $db;
    $stmt = $db->prepare(
        "UPDATE invoices SET status = 'paid', paid_at = ? WHERE external_id = ?"
    );
    $stmt->execute([$invoice['paid_on'], $invoice['id']]);
    sendPaymentConfirmation($invoice['email'], $invoice);
}

function handlePaymentFailed($invoice) {
    global $db;
    $stmt = $db->prepare(
        "INSERT INTO payment_failures (invoice_id, failed_at) VALUES (?, NOW())"
    );
    $stmt->execute([$invoice['id']]);
    sendPaymentFailedEmail($invoice['email'], $invoice);
}
?>

Webhook URL Requirements

  • Must be a valid HTTP or HTTPS URL
  • Must accept POST requests with Content-Type: application/json
  • Should return a 200299 status code to acknowledge receipt
  • Should respond within 10 seconds

Webhook URLs are provided at invoice creation time via the webhook_url field in the request body. There is no separate webhook registration endpoint.

Testing Webhooks

Local Development

For local testing, use a tunnel service to expose your local server:

# Using ngrok
ngrok http 3000

# Use the HTTPS URL as your webhook parameter
# Example: https://abc123.ngrok.io/webhook/safepays

Alternatively, use Webhook.site to inspect incoming webhook payloads without writing any code.

Test Mode

When using test API keys:

  1. Create invoices normally
  2. Mark them as paid through the SafePays dashboard
  3. Your webhook URL will receive the notification with test payment data

Best Practices

  1. Respond quickly — Return 200 OK immediately and process the webhook asynchronously if needed
  2. Handle duplicates — Webhooks may be delivered more than once; use the invoice id to deduplicate
  3. Use HTTPS — Always use HTTPS endpoints for webhook URLs
  4. Verify the data — Use the Check Invoice Status endpoint to confirm payment details if needed
  5. Log everything — Log all webhook payloads for debugging and reconciliation

On this page