Webhooks
Overview
Webhooks allow you to receive real-time notifications when events occur in your Alternative Payments account. Instead of continuously polling the API to check for updates, webhooks push event data directly to your server as they happen.
Available Webhook Topics
Subscribe to the following webhook topics to receive notifications:
payout_paid
Confirms that a payout was successfully settled.
payout_failed
Indicates a failed payout attempt.
customer_created
Triggered when a new customer is onboarded.
payment_succeeded
Confirms that funds were successfully captured.
payment_chargeback
Notifies when a dispute occurs. Impacts reconciliation and may require intervention.
payment_failed
Indicates a failed payment attempt.
invoice_paid
Confirms the invoice has been fully paid. Closes the accounts receivable lifecycle and links the payment to the customer obligation.
payment_method_added
Indicates a payment method was added. Useful for enabling Autopay.
payment_method_deleted
Indicates a payment method was removed. Also relevant for managing Autopay.
Managing Webhook Subscriptions
Subscribe to a Webhook
Create a new webhook subscription to start receiving event notifications.
Endpoint: POST /webhooks
Request Body:
{
"endpoint_url": "https://your-server.com/webhooks",
"secret_key": "your_secret_key",
"topic": "invoice_paid"
}Parameters:
endpoint_url(required) - Your HTTPS endpoint that will receive webhook eventssecret_key(optional) - A secret key for authentication. If provided, it will be sent as a Bearer token in theAuthorizationheadertopic(required) - The event type you want to subscribe to (see available topics above)
List Webhook Subscriptions
View all your active and inactive webhook subscriptions.
Endpoint: GET /webhooks
Query Parameters:
limit(optional, default: 100) - Number of items to returnafter(optional) - Cursor for forward paginationbefore(optional) - Cursor for backward paginationtopic(optional) - Filter by webhook topicis_active(optional) - Filter by active status (true/false)
Unsubscribe from a Webhook
Remove a webhook subscription when you no longer want to receive notifications.
Endpoint: DELETE /webhooks/{subscription_id}
Parameters:
subscription_id(required) - The ID of the subscription to remove
Webhook Events
List Webhook Events
View the history of webhook events sent to your endpoints.
Endpoint: GET /webhooks/events
Query Parameters:
limit(optional, default: 100) - Number of items to returnafter(optional) - Cursor for forward paginationbefore(optional) - Cursor for backward paginationtopic(optional) - Filter by webhook topicstatus(optional) - Filter by status:pending,sent, orfailedfrom_date(optional) - Filter events created after this date (ISO 8601)to_date(optional) - Filter events created before this date (ISO 8601)
Webhook Payload Structure
All webhooks are sent as HTTP POST requests with a JSON payload containing the following fields:
{
"entity_id": "5cbcf9c3-9378-4633-91f0-886fa172f360",
"idempotency_key": "066a3fd0-b849-494f-87ba-d186a6e4b2cc",
"timestamp": "2025-09-29T21:01:36Z",
"topic": "invoice_paid",
"data": {
"customer_id": "170d05e3-b498-4547-af7c-985f1e85d9f7",
"invoice_id": "5cbcf9c3-9378-4633-91f0-886fa172f360",
"status": "paid"
}
}Payload Fields
entity_id- The ID of the primary resource related to this eventFor
invoice_paid→ Invoice IDFor
payment_method_added→ Payment Method IDFor
customer_created→ Customer IDFor
payout_paid→ Payout ID
idempotency_key- A unique identifier for this webhook event. Use this to prevent processing duplicate webhookstimestamp- ISO 8601 timestamp of when the event occurredtopic- The event type (see Available Webhook Topics)data- Additional context about the event. The structure varies by topic but typically includes:Related entity IDs (customer_id, invoice_id, etc.)
Current status of the resource
Other relevant information specific to the event type
Important: The entity_id field always contains the ID of the primary subject of the webhook. Use this field to quickly identify which resource triggered the event, then use the additional fields in data for complete context.
Webhook Ordering
Webhooks are delivered in order. This means:
All webhooks for your account are sent sequentially based on when they were created
A webhook will not be sent until all previous webhooks have been successfully delivered
This ensures you receive events in the correct chronological order
Retry Logic
If your endpoint is temporarily unavailable or returns an error, Alternative Payments will automatically retry webhook delivery.
Retry Behavior
Maximum attempts: 5 retries
Backoff strategy: Exponential backoff with a 30-second base delay
Retry intervals: 1:30m, 4:30m, 13:30m, 40:30m, 2:02hrs (attempts 1-5)
The retry delay is calculated using the formula:
delay = 30 seconds × (3 ^ attempt_number)What Happens After Max Retries
After reaching the maximum number of retry attempts:
The webhook delivery is permanently paused for your account
All pending and failed webhooks remain in queue but won't be automatically retried
You must manually trigger a retry to resume webhook delivery
Manually Retrying Failed Webhooks
When webhooks reach max retry attempts, use this endpoint to resume delivery:
Endpoint: POST /webhooks/retry
Request: No body required
Response:
{
"message": "success"
}This endpoint will:
Reset the retry counter
Resume processing all pending and failed webhooks in order
Attempt to deliver webhooks again with the full retry logic
Critical: If webhook delivery fails repeatedly, you must fix the underlying issue with your endpoint before manually retrying. Otherwise, webhooks will fail again and enter the same paused state.
Processing Limits
To protect your endpoint from being overwhelmed:
The retry process sends a maximum of 10 webhooks per run
If more than 10 webhooks are pending, they will be processed in subsequent retry cycles
All webhooks are still delivered in order
Best Practices
Endpoint Requirements
Use HTTPS - Webhook endpoints must use HTTPS (not HTTP)
Return 2xx status codes - Your endpoint should return a 200-299 status code to indicate successful receipt
Respond quickly - Process webhooks asynchronously if possible. Acknowledge receipt immediately and process the event in the background
Handle timeouts - Implement proper timeout handling on your server
Security
Verify webhook authenticity:
Set a
secret_keywhen subscribingValidate the
Authorizationheader on incoming webhook requestsCheck that the Bearer token matches your secret
Use idempotency keys:
Store the
idempotency_keyfrom each webhookCheck for duplicate keys before processing to prevent double-processing
Validate payload structure:
Always validate the JSON structure before processing
Check that required fields are present
Verify the
topicmatches your expected event type
Error Handling
Return proper status codes:
200-299: Successfully received and will process300-599: Error (we will retry)
Monitor webhook events:
Use
GET /webhooks/eventsto monitor delivery statusSet up alerts for failed webhooks
Check your webhook endpoint logs regularly
Testing
Before going live:
Subscribe to a webhook topic in your staging/test environment
Trigger test events (create a customer, pay an invoice, etc.)
Verify your endpoint receives and processes the webhooks correctly
Test your retry logic by returning error status codes temporarily
Confirm your secret key validation works correctly
Example Implementation
Here's a basic example of a webhook endpoint handler:
import express, { Request, Response } from "express";
import crypto from "crypto";
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = "your_secret_key";
const processedWebhooks = new Set<string>();
interface WebhookPayload {
idempotency_key: string;
topic: "invoice_paid" | "payment_failed" | string;
entity_id: string;
data?: Record<string, any>;
}
app.post("/webhooks", (req: Request, res: Response) => {
// Verify authentication
const authHeader = req.headers.authorization ?? "";
if (!authHeader.startsWith("Bearer ")) {
return res.status(401).json({ error: "Unauthorized" });
}
const token = authHeader.replace("Bearer ", "");
if (token !== WEBHOOK_SECRET) {
return res.status(401).json({ error: "Invalid token" });
}
// Parse webhook payload
const payload = req.body as WebhookPayload;
const { idempotency_key, topic, entity_id, data = {} } = payload;
if (!idempotency_key) {
return res.status(400).json({ error: "Missing idempotency key" });
}
// Check for duplicates
if (processedWebhooks.has(idempotency_key)) {
return res.json({ message: "Already processed" });
}
// Handle topics
switch (topic) {
case "invoice_paid": {
const invoiceId = entity_id;
const customerId = data.customer_id;
console.log(`Invoice ${invoiceId} paid by customer ${customerId}`);
break;
}
case "payment_failed": {
const paymentId = entity_id;
console.log(`Payment ${paymentId} failed`);
break;
}
default:
console.log(`Unhandled topic: ${topic}`);
}
// Mark as processed
processedWebhooks.add(idempotency_key);
return res.json({ message: "Webhook received" });
});
const PORT = 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Troubleshooting
For additional help, refer to the FAQ or contact our support team.
Last updated
Was this helpful?
