# 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:

| Topic                            | Description                                                                                                                          |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `payout_paid`                    | Confirms that a payout was successfully settled.                                                                                     |
| `payout_processing`              | Indicates the payout has started to be processed                                                                                     |
| `payout_scheduled`               | Indicates the payout is scheduled to be processed                                                                                    |
| `payout_failed`                  | Indicates a failed payout attempt.                                                                                                   |
| `customer_created`               | Triggered when a new customer is onboarded.                                                                                          |
| `customer_updated`               | Triggered when the customer has their information updated.                                                                           |
| `customer_archived`              | Triggered when a customer is archived.                                                                                               |
| `payment_succeeded`              | Confirms that funds were successfully captured.                                                                                      |
| `payment_refunded`               | Triggered when a payment refund request occurs.                                                                                      |
| `payment_chargeback`             | Notifies when a dispute occurs. Impacts reconciliation and may require intervention.                                                 |
| `payment_failed`                 | Indicates a failed payment attempt.                                                                                                  |
| `invoice_created`                | Triggered when a new invoice is created.                                                                                             |
| `invoice_updated`                | Triggered when invoice information is updated.                                                                                       |
| `invoice_paid`                   | Confirms the invoice has been fully paid. Closes the accounts receivable lifecycle and links the payment to the customer obligation. |
| `invoice_archived`               | Triggered when the invoice is archived.                                                                                              |
| `payment_method_added`           | Indicates a payment method was added. Useful for enabling Autopay.                                                                   |
| `default_payment_method_changed` | Triggered when the customer default payment method change.                                                                           |
| `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:**

```json
{
  "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 events
* `secret_key` (optional) - A secret key for authentication. If provided, it will be sent as a Bearer token in the `Authorization` header
* `topic` (required) - The event type you want to subscribe to (see available topics above)

{% hint style="info" %}
**Authentication:** If you provide a `secret_key`, all webhook requests to your endpoint will include it in the `Authorization` header as a Bearer token. Use this to verify the authenticity of webhook requests.
{% endhint %}

### List Webhook Subscriptions

View all your active and inactive webhook subscriptions.

**Endpoint:** `GET /webhooks`

**Query Parameters:**

* `limit` (optional, default: 100) - Number of items to return
* `after` (optional) - Cursor for forward pagination
* `before` (optional) - Cursor for backward pagination
* `topic` (optional) - Filter by webhook topic
* `is_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 return
* `after` (optional) - Cursor for forward pagination
* `before` (optional) - Cursor for backward pagination
* `topic` (optional) - Filter by webhook topic
* `status` (optional) - Filter by status: `pending`, `sent`, or `failed`
* `from_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:

```json
{
  "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 event
  * For `invoice_paid` → Invoice ID
  * For `payment_method_added` → Payment Method ID
  * For `customer_created` → Customer ID
  * For `payout_paid` → Payout ID
* **`idempotency_key`** - A unique identifier for this webhook event. Use this to prevent processing duplicate webhooks
* **`timestamp`** - ISO 8601 timestamp of when the event occurred
* **`topic`** - 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

{% hint style="warning" %}
**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.
{% endhint %}

## 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:

1. The webhook delivery is **permanently paused** for your account
2. All pending and failed webhooks remain in queue but won't be automatically retried
3. **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:**

```json
{
  "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

{% hint style="danger" %}
**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.
{% endhint %}

## 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

1. **Verify webhook authenticity:**
   * Set a `secret_key` when subscribing
   * Validate the `Authorization` header on incoming webhook requests
   * Check that the Bearer token matches your secret
2. **Use idempotency keys:**
   * Store the `idempotency_key` from each webhook
   * Check for duplicate keys before processing to prevent double-processing
3. **Validate payload structure:**
   * Always validate the JSON structure before processing
   * Check that required fields are present
   * Verify the `topic` matches your expected event type

### Error Handling

* **Return proper status codes:**
  * `200-299`: Successfully received and will process
  * `300-599`: Error (we will retry)
* **Monitor webhook events:**
  * Use `GET /webhooks/events` to monitor delivery status
  * Set up alerts for failed webhooks
  * Check your webhook endpoint logs regularly

### Testing

Before going live:

1. Subscribe to a webhook topic in your staging/test environment
2. Trigger test events (create a customer, pay an invoice, etc.)
3. Verify your endpoint receives and processes the webhooks correctly
4. Test your retry logic by returning error status codes temporarily
5. Confirm your secret key validation works correctly

***

## Example Implementation

Here's a basic example of a webhook endpoint handler:

```typescript
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

<details>

<summary><strong>My webhooks are not being delivered</strong></summary>

Check the following:

1. Verify your endpoint is publicly accessible via HTTPS
2. Confirm your endpoint returns a 2xx status code
3. Check `GET /webhooks/events` to see webhook status
4. Ensure your subscription is active (`is_active: true`)
5. Verify your server isn't blocking incoming requests

</details>

<details>

<summary><strong>Webhooks stopped after reaching max retries</strong></summary>

1. Fix the issue preventing webhook delivery (check your server logs)
2. Call `POST /webhooks/retry` to resume delivery
3. Monitor the webhook events to confirm successful delivery

</details>

<details>

<summary><strong>Receiving duplicate webhooks</strong></summary>

This can happen during retries. Always:

* Check the `idempotency_key` before processing
* Store processed keys in your database
* Skip processing if the key was already handled

</details>

<details>

<summary><strong>How do I test webhooks locally?</strong></summary>

Use a tool like [ngrok](https://ngrok.com/) to expose your local server:

1. Run `ngrok http 5000` (or your local port)
2. Use the HTTPS URL provided by ngrok as your `endpoint_url`
3. Subscribe to a webhook with the ngrok URL
4. Trigger events in your test environment

</details>

***

For additional help, refer to the FAQ or contact our support team.
