SafePays API

Webhooks

Real-time notifications for payment events

Overview

Webhooks provide real-time notifications when invoice events occur, particularly payment status changes. When creating an invoice, you must provide a webhook URL that will receive POST requests with event data.

How Webhooks Work

Register Webhook URL

Provide your webhook endpoint URL when creating an invoice

Event Occurs

Customer makes a payment or invoice status changes

Webhook Triggered

SafePays sends a POST request to your webhook URL

Process Event

Your server processes the webhook and updates your system

Respond

Return a 200 OK status to acknowledge receipt

Webhook Events

Currently, webhooks are triggered for the following events:

EventDescriptionInvoice Status
Payment SuccessPayment completed successfullyPaid
Payment FailedPayment attempt failedFailed

Webhook Payload

When an event occurs, SafePays sends a POST request with the complete invoice data.

Request Headers

Content-Type: application/json
X-SafePays-Event: payment_success
X-SafePays-Signature: sha256_signature_here

Payload Structure

{
  "event": "payment_success",
  "timestamp": "2024-01-15T10:30:00Z",
  "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"
  }
}

Implementing a Webhook Endpoint

Your webhook endpoint should:

  1. Receive POST requests
  2. Verify the webhook signature (recommended)
  3. Process the event data
  4. Return a 200 OK response quickly

Example Implementations

// Express.js webhook handler
app.post('/webhook/safepays', express.json(), async (req, res) => {
  try {
    const { event, invoice } = req.body;
    
    // Verify webhook signature (recommended)
    const signature = req.headers['x-safepays-signature'];
    if (!verifyWebhookSignature(req.body, signature)) {
      return res.status(401).send('Invalid signature');
    }
    
    // Process based on event type
    switch (event) {
      case 'payment_success':
        await handlePaymentSuccess(invoice);
        break;
      case 'payment_failed':
        await handlePaymentFailed(invoice);
        break;
      default:
        console.log('Unknown event:', event);
    }
    
    // Return 200 OK immediately
    res.status(200).json({ received: true });
    
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).send('Webhook processing failed');
  }
});

async function handlePaymentSuccess(invoice) {
  // Update your database
  await db.invoices.update({
    where: { external_id: invoice.id },
    data: { 
      status: 'paid',
      paid_at: invoice.paid_on
    }
  });
  
  // Send confirmation email
  await sendPaymentConfirmation(invoice.email, invoice);
  
  // Update inventory, activate subscription, etc.
  await fulfillOrder(invoice);
}

async function handlePaymentFailed(invoice) {
  // Log failed payment
  await db.payment_failures.create({
    invoice_id: invoice.id,
    failed_at: new Date()
  });
  
  // Notify customer
  await sendPaymentFailedEmail(invoice.email, invoice);
}
from flask import Flask, request, jsonify
import json
import hashlib
import hmac

app = Flask(__name__)

@app.route('/webhook/safepays', methods=['POST'])
def safepays_webhook():
    try:
        # Parse webhook data
        data = request.get_json()
        event = data.get('event')
        invoice = data.get('invoice')
        
        # Verify webhook signature (recommended)
        signature = request.headers.get('X-SafePays-Signature')
        if not verify_webhook_signature(request.data, signature):
            return jsonify({'error': 'Invalid signature'}), 401
        
        # Process based on event type
        if event == 'payment_success':
            handle_payment_success(invoice)
        elif event == 'payment_failed':
            handle_payment_failed(invoice)
        else:
            print(f'Unknown event: {event}')
        
        # Return 200 OK immediately
        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):
    # Update your database
    db.execute(
        "UPDATE invoices SET status = 'paid', paid_at = ? WHERE external_id = ?",
        (invoice['paid_on'], invoice['id'])
    )
    
    # Send confirmation email
    send_payment_confirmation(invoice['email'], invoice)
    
    # Fulfill order
    fulfill_order(invoice)

def handle_payment_failed(invoice):
    # Log failed payment
    db.execute(
        "INSERT INTO payment_failures (invoice_id, failed_at) VALUES (?, ?)",
        (invoice['id'], datetime.now())
    )
    
    # Notify customer
    send_payment_failed_email(invoice['email'], invoice)

def verify_webhook_signature(payload, signature):
    # Implement signature verification
    # This is a placeholder - actual implementation depends on SafePays signature method
    secret = os.environ.get('SAFEPAYS_WEBHOOK_SECRET')
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
<?php
// webhook.php - SafePays webhook handler

// Get the raw POST data
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);

// Verify webhook signature (recommended)
$signature = $_SERVER['HTTP_X_SAFEPAYS_SIGNATURE'] ?? '';
if (!verifyWebhookSignature($payload, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Extract event data
$event = $data['event'] ?? '';
$invoice = $data['invoice'] ?? [];

try {
    // Process based on event type
    switch ($event) {
        case 'payment_success':
            handlePaymentSuccess($invoice);
            break;
        case 'payment_failed':
            handlePaymentFailed($invoice);
            break;
        default:
            error_log('Unknown event: ' . $event);
    }
    
    // Return 200 OK
    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;
    
    // Update invoice status
    $stmt = $db->prepare("UPDATE invoices SET status = 'paid', paid_at = ? WHERE external_id = ?");
    $stmt->execute([$invoice['paid_on'], $invoice['id']]);
    
    // Send confirmation email
    sendPaymentConfirmation($invoice['email'], $invoice);
    
    // Fulfill order
    fulfillOrder($invoice);
}

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

function verifyWebhookSignature($payload, $signature) {
    $secret = $_ENV['SAFEPAYS_WEBHOOK_SECRET'];
    $expected = hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}
?>
# webhook_controller.rb - Rails webhook handler

class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token
  
  def safepays
    begin
      # Parse webhook data
      payload = request.raw_post
      data = JSON.parse(payload)
      
      # Verify webhook signature (recommended)
      signature = request.headers['X-SafePays-Signature']
      unless verify_webhook_signature(payload, signature)
        render json: { error: 'Invalid signature' }, status: :unauthorized
        return
      end
      
      # Process based on event type
      case data['event']
      when 'payment_success'
        handle_payment_success(data['invoice'])
      when 'payment_failed'
        handle_payment_failed(data['invoice'])
      else
        Rails.logger.info "Unknown event: #{data['event']}"
      end
      
      # Return 200 OK
      render json: { received: true }, status: :ok
      
    rescue => e
      Rails.logger.error "Webhook error: #{e.message}"
      render json: { error: 'Webhook processing failed' }, status: :internal_server_error
    end
  end
  
  private
  
  def handle_payment_success(invoice)
    # Update invoice status
    Invoice.where(external_id: invoice['id']).update(
      status: 'paid',
      paid_at: invoice['paid_on']
    )
    
    # Send confirmation email
    PaymentMailer.confirmation(invoice['email'], invoice).deliver_later
    
    # Fulfill order
    OrderFulfillment.new(invoice).process
  end
  
  def handle_payment_failed(invoice)
    # Log failed payment
    PaymentFailure.create(
      invoice_id: invoice['id'],
      failed_at: Time.current
    )
    
    # Notify customer
    PaymentMailer.failed(invoice['email'], invoice).deliver_later
  end
  
  def verify_webhook_signature(payload, signature)
    secret = ENV['SAFEPAYS_WEBHOOK_SECRET']
    expected = OpenSSL::HMAC.hexdigest('SHA256', secret, payload)
    ActiveSupport::SecurityUtils.secure_compare(expected, signature)
  end
end

Webhook Security

Signature Verification

Always verify webhook signatures to ensure requests are from SafePays and haven't been tampered with.

SafePays signs webhook payloads using HMAC-SHA256. The signature is included in the X-SafePays-Signature header.

To verify:

  1. Compute HMAC-SHA256 of the raw request body using your webhook secret
  2. Compare with the signature in the header
  3. Use constant-time comparison to prevent timing attacks

Security Best Practices

  1. Use HTTPS: Always use HTTPS endpoints for webhooks
  2. Verify Signatures: Implement signature verification
  3. Idempotency: Handle duplicate webhooks gracefully
  4. Quick Response: Return 200 OK quickly, process asynchronously if needed
  5. Error Handling: Log failures but don't expose internal errors
  6. IP Whitelisting: Consider restricting access to SafePays IPs (contact support)

Testing Webhooks

Local Development

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

# Install ngrok
npm install -g ngrok

# Start your local server (e.g., port 3000)
node server.js

# In another terminal, create tunnel
ngrok http 3000

# Use the HTTPS URL provided by ngrok as your webhook URL
# Example: https://abc123.ngrok.io/webhook/safepays
# Install localtunnel
npm install -g localtunnel

# Start your local server (e.g., port 3000)
node server.js

# Create tunnel
lt --port 3000 --subdomain yourapp

# Use the URL as your webhook URL
# Example: https://yourapp.loca.lt/webhook/safepays
1. Visit https://webhook.site
2. Copy your unique URL
3. Use as webhook URL when creating invoices
4. View incoming webhooks in real-time on the website
5. Great for inspecting webhook structure without coding

Test Mode

When using test API keys:

  1. Create test invoices normally
  2. Mark them as paid in the dashboard
  3. Webhooks fire with test data
  4. No real payments are processed

Handling Webhook Failures

Retry Policy

SafePays retries failed webhook deliveries:

  • Initial attempt: Immediate
  • 1st retry: After 1 minute
  • 2nd retry: After 5 minutes
  • 3rd retry: After 15 minutes
  • 4th retry: After 1 hour
  • 5th retry: After 4 hours

After 5 failed attempts, the webhook is marked as failed.

Response Requirements

Your endpoint should:

  • Return HTTP status 200-299 for success
  • Respond within 10 seconds
  • Return errors (400-599) for failures

Error Recovery

If webhooks fail:

  1. Check the dashboard for failed webhook logs
  2. Use the Get Invoice Status endpoint to check current status
  3. Manually reconcile any missed events
  4. Fix webhook endpoint issues
  5. Contact support to retry failed webhooks

Common Issues

Duplicate Webhooks

Webhooks may be delivered more than once. Implement idempotency:

// Store processed webhook IDs
const processedWebhooks = new Set();

function handleWebhook(webhookId, invoice) {
  if (processedWebhooks.has(webhookId)) {
    console.log('Webhook already processed:', webhookId);
    return;
  }
  
  // Process webhook
  processInvoice(invoice);
  
  // Mark as processed
  processedWebhooks.add(webhookId);
}

Timeout Issues

If processing takes time, respond immediately and process asynchronously:

app.post('/webhook', (req, res) => {
  // Respond immediately
  res.status(200).json({ received: true });
  
  // Process asynchronously
  setImmediate(() => {
    processWebhook(req.body);
  });
});

Missing Webhooks

If you suspect missing webhooks:

  1. Check your server logs for errors
  2. Verify webhook URL is correct
  3. Check firewall/security settings
  4. Use webhook testing tools
  5. Contact SafePays support

Webhook Events Roadmap

Coming soon:

  • invoice.created - When invoice is created
  • invoice.updated - When invoice is modified
  • invoice.cancelled - When invoice is cancelled
  • customer.created - When customer is created
  • customer.updated - When customer is modified

Subscribe to our changelog to stay updated on new webhook events and features.

On this page