> ## Documentation Index
> Fetch the complete documentation index at: https://docs.withgale.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications about events in your Gale account

# Webhooks

Webhooks allow your application to receive real-time notifications when events happen in your Gale account, such as completed payments, failed charges, and refunds.

## How Webhooks Work

1. **Customer** completes payment on Gale's checkout page
2. **Gale** processes the payment
3. **Gale sends** a POST request to your webhook endpoint with event data
4. **Your server** processes the event (e.g., fulfills order, sends email)
5. **Your server** returns a `200 OK` response
6. **Event is marked** as delivered successfully

If your server doesn't respond with `200`, Gale retries the delivery automatically.

## Setup

### 1. Create an Endpoint on Your Server

```javascript theme={null}
app.post('/webhooks/gale', async (req, res) => {
  // Acknowledge immediately — process async
  res.status(200).send('OK');

  const event = req.body;

  switch (event.type) {
    case 'order.completed':
      await fulfillOrder(event.data);
      break;
    case 'order.failed':
      await notifyOrderFailed(event.data);
      break;
    case 'refund.succeeded':
      await handleRefund(event.data);
      break;
  }
});
```

### 2. Register It in the Dashboard

1. Log in to [Gale Dashboard](https://dashboard.withgale.com)
2. Go to **Settings** → **Webhooks**
3. Click **Add Endpoint**
4. Enter your webhook URL
5. Select the events to subscribe to
6. Copy the secret shown — **it is only displayed once**

## Authentication

Every request Gale sends to your endpoint includes a Bearer token in the `Authorization` header:

```
Authorization: Bearer <your-endpoint-secret>
```

Validate this on your server to confirm the request is genuinely from Gale:

```javascript theme={null}
app.post('/webhooks/gale', (req, res) => {
  const token = req.headers['authorization']?.replace('Bearer ', '');

  if (token !== process.env.GALE_WEBHOOK_SECRET) {
    return res.status(401).send('Unauthorized');
  }

  res.status(200).send('OK');
  // process event...
});
```

## Retry Schedule

If your endpoint doesn't return `200`, Gale retries with increasing delays:

| Attempt   | Delay after previous failure |
| --------- | ---------------------------- |
| 1st retry | 1 minute                     |
| 2nd retry | 5 minutes                    |
| 3rd retry | 30 minutes                   |
| 4th retry | 2 hours                      |
| 5th retry | 6 hours                      |

After all retries are exhausted the delivery is marked as failed. You can see failed deliveries in the dashboard under **Settings → Webhooks**.

## Webhook Events

| Event              | When it fires                 |
| ------------------ | ----------------------------- |
| `order.completed`  | Payment captured successfully |
| `order.failed`     | Payment failed or declined    |
| `refund.created`   | Refund initiated by merchant  |
| `refund.succeeded` | Refund processed successfully |
| `refund.failed`    | Refund attempt failed         |

## Event Envelope

Every event shares the same top-level structure:

```json theme={null}
{
  "id": "evt_01ABC...",
  "type": "order.completed",
  "created_at": "2026-03-31T12:00:00Z",
  "data": { ... }
}
```

Use `id` for deduplication — Gale may deliver the same event more than once during retries.

## Event Payload Examples

### order.completed

Fires when payment is captured and the order is complete. Use this to trigger fulfillment.

```json theme={null}
{
  "id": "evt_01ABC...",
  "type": "order.completed",
  "created_at": "2026-03-31T12:00:00Z",
  "data": {
    "id": "ord_01ABC...",
    "checkout_id": "cs_abc123",
    "status": "completed",
    "is_recurring": false,
    "payment_status": "captured",
    "amount": 4995,
    "currency": "usd",
    "payment_link_id": "pl_abc123",
    "client_reference_id": "your-order-123",
    "reference_id": null,
    "customer": {
      "email": "jane@example.com",
      "name": "Jane Doe"
    },
    "line_items": [
      {
        "product_id": "PROD-001",
        "name": "Digital Thermometer",
        "quantity": 1,
        "unit_amount": 4995,
        "amount": 4995,
        "currency": "usd",
        "is_recurring": false
      }
    ],
    "created_at": "2026-03-31T12:00:00Z"
  }
}
```

<Note>
  `payment_link_id` and `client_reference_id` are present for payment link orders. For checkout session orders, `reference_id` is set instead and `payment_link_id` is null.
</Note>

### order.failed

Fires when payment fails or is declined.

```json theme={null}
{
  "id": "evt_01DEF...",
  "type": "order.failed",
  "created_at": "2026-03-31T12:01:00Z",
  "data": {
    "id": "ord_01DEF...",
    "checkout_id": "cs_def456",
    "status": "failed",
    "is_recurring": false,
    "payment_status": "failed",
    "amount": 4995,
    "currency": "usd",
    "payment_link_id": "pl_abc123",
    "client_reference_id": "your-order-123",
    "reference_id": null,
    "failure_reason": "Payment declined",
    "customer": {
      "email": "jane@example.com",
      "name": "Jane Doe"
    },
    "created_at": "2026-03-31T12:01:00Z"
  }
}
```

### refund.created

Fires when a refund is initiated. A `refund.succeeded` or `refund.failed` will follow.

```json theme={null}
{
  "id": "evt_01GHI...",
  "type": "refund.created",
  "created_at": "2026-03-31T13:00:00Z",
  "data": {
    "id": "ref_01GHI...",
    "order_id": "ord_01ABC...",
    "checkout_id": "cs_abc123",
    "amount": 4995,
    "currency": "usd",
    "reason": "Customer request",
    "payment_link_id": "pl_abc123",
    "client_reference_id": "your-order-123",
    "reference_id": null,
    "customer": {
      "email": "jane@example.com",
      "name": "Jane Doe"
    },
    "initiated_at": "2026-03-31T13:00:00Z"
  }
}
```

### refund.succeeded

Fires when the refund is processed. Check `payment_status` to know if the order is fully or partially refunded.

```json theme={null}
{
  "id": "evt_01JKL...",
  "type": "refund.succeeded",
  "created_at": "2026-03-31T13:01:00Z",
  "data": {
    "id": "ref_01GHI...",
    "order_id": "ord_01ABC...",
    "checkout_id": "cs_abc123",
    "amount": 4995,
    "currency": "usd",
    "order_status": "completed",
    "payment_status": "refunded",
    "reason": "Customer request",
    "payment_link_id": "pl_abc123",
    "client_reference_id": "your-order-123",
    "reference_id": null,
    "customer": {
      "email": "jane@example.com",
      "name": "Jane Doe"
    },
    "refunded_at": "2026-03-31T13:01:00Z"
  }
}
```

`payment_status` will be `refunded` for a full refund or `partially_refunded` if only part of the order was refunded.

### refund.failed

Fires when the refund attempt fails.

```json theme={null}
{
  "id": "evt_01MNO...",
  "type": "refund.failed",
  "created_at": "2026-03-31T13:02:00Z",
  "data": {
    "id": "ref_01GHI...",
    "order_id": "ord_01ABC...",
    "checkout_id": "cs_abc123",
    "amount": 4995,
    "currency": "usd",
    "failure_reason": "Processor declined refund",
    "payment_link_id": "pl_abc123",
    "client_reference_id": "your-order-123",
    "reference_id": null,
    "customer": {
      "email": "jane@example.com",
      "name": "Jane Doe"
    },
    "failed_at": "2026-03-31T13:02:00Z"
  }
}
```

## Status Fields

### `status` — Fulfillment status

| Value       | Meaning                            |
| ----------- | ---------------------------------- |
| `completed` | Payment captured, ready to fulfill |
| `failed`    | Payment failed                     |
| `cancelled` | Order cancelled                    |

### `payment_status` — Payment state

| Value                | Meaning                       |
| -------------------- | ----------------------------- |
| `captured`           | Payment successfully captured |
| `failed`             | Payment failed or declined    |
| `refunded`           | Fully refunded                |
| `partially_refunded` | Partially refunded            |

## Best Practices

* **Return 200 immediately**, then process the event asynchronously
* **Deduplicate using `id`** — Gale may send the same event more than once during retries
* **Don't rely on webhooks alone** — use `GET /api/v2/checkout/{checkout_id}` to poll status if a webhook is missed
* **Always validate the Bearer token** before processing

## Missed Webhooks — Status Polling

If your server was down or a webhook was missed, you can check order status directly:

```bash theme={null}
GET /api/v2/checkout/{checkout_id}
```

[See the endpoint reference →](/api-reference/endpoint/get-checkout)

## Related Resources

* [Payment Links](/integration/payment-links)
* [Order Object](/api-reference/objects/order)
* [Get Checkout Status](/api-reference/endpoint/get-checkout)
