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:
| Event | Description | Invoice Status |
|---|---|---|
| Payment Success | Payment completed successfully | Paid |
| Payment Failed | Payment attempt failed | Failed |
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_herePayload 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:
- Receive POST requests
- Verify the webhook signature (recommended)
- Process the event data
- 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
endWebhook 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:
- Compute HMAC-SHA256 of the raw request body using your webhook secret
- Compare with the signature in the header
- Use constant-time comparison to prevent timing attacks
Security Best Practices
- Use HTTPS: Always use HTTPS endpoints for webhooks
- Verify Signatures: Implement signature verification
- Idempotency: Handle duplicate webhooks gracefully
- Quick Response: Return 200 OK quickly, process asynchronously if needed
- Error Handling: Log failures but don't expose internal errors
- 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/safepays1. 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 codingTest Mode
When using test API keys:
- Create test invoices normally
- Mark them as paid in the dashboard
- Webhooks fire with test data
- 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:
- Check the dashboard for failed webhook logs
- Use the Get Invoice Status endpoint to check current status
- Manually reconcile any missed events
- Fix webhook endpoint issues
- 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:
- Check your server logs for errors
- Verify webhook URL is correct
- Check firewall/security settings
- Use webhook testing tools
- Contact SafePays support
Webhook Events Roadmap
Coming soon:
invoice.created- When invoice is createdinvoice.updated- When invoice is modifiedinvoice.cancelled- When invoice is cancelledcustomer.created- When customer is createdcustomer.updated- When customer is modified
Subscribe to our changelog to stay updated on new webhook events and features.
Related Resources
- Create Invoice - Set webhook URL
- Error Handling - Handle API errors
- Authentication - Secure your endpoints