> ## 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` | The order is terminally closed and will not complete |
| `payment.failed` | A single payment attempt was declined; the shopper may retry on the same checkout |
| `refund.created` | Refund initiated by merchant |
| `refund.succeeded` | Refund processed successfully |
| `refund.failed` | Refund attempt failed (including failures after submission) |
| `dispute.created` | A chargeback was opened against an order — respond before `evidence_due_by` |
| `dispute.closed` | The chargeback was resolved (`outcome`: `won` / `lost`) |
| `checkout.expired` | A checkout session expired without payment |

## 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 the order is terminally closed and will not complete. For a declined attempt the shopper can still retry, use `payment.failed`.

```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"
  }
}
```

### payment.failed

Fires when a single payment attempt is declined. The shopper may retry on the same checkout, so this is not terminal. `retryable` is `false` for a hard decline. Subscribe to this event to observe declined attempts; use `order.failed` for the terminal outcome.

```json theme={null}
{
  "id": "evt_01GHI...",
  "type": "payment.failed",
  "created_at": "2026-03-31T12:01:00Z",
  "data": {
    "id": "ord_01DEF...",
    "checkout_id": "cs_def456",
    "status": "failed",
    "payment_status": "failed",
    "decline_code": "do_not_honor",
    "decline_reason": "issuer_declined",
    "decline_severity": "soft",
    "retryable": true,
    "failure_reason": "Your card was declined.",
    "amount": 4995,
    "currency": "usd",
    "payment_link_id": null,
    "client_reference_id": "your-order-123",
    "reference_id": null,
    "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"
  }
}
```

### dispute.created

Fires when a customer opens a chargeback (dispute) against an order. **Respond before `evidence_due_by`** — missed deadlines are lost disputes.

```json theme={null}
{
  "id": "evt_01PQR...",
  "type": "dispute.created",
  "created_at": "2026-03-31T14:00:00Z",
  "data": {
    "id": "dp_01STU...",
    "order_id": "ord_01ABC...",
    "checkout_id": "cs_abc123",
    "amount": 4995,
    "currency": "usd",
    "reason": "fraudulent",
    "status": "needs_response",
    "evidence_due_by": "2026-04-10T00:00:00Z",
    "payment_link_id": null,
    "client_reference_id": null,
    "reference_id": "your-order-123",
    "customer": {
      "email": "jane@example.com",
      "name": "Jane Doe"
    },
    "opened_at": "2026-03-31T14:00:00Z"
  }
}
```

### dispute.closed

Fires when a chargeback is resolved. `outcome` is `won` (funds returned to you), `lost`, or `warning_closed`.

```json theme={null}
{
  "id": "evt_01VWX...",
  "type": "dispute.closed",
  "created_at": "2026-04-12T09:00:00Z",
  "data": {
    "id": "dp_01STU...",
    "order_id": "ord_01ABC...",
    "checkout_id": "cs_abc123",
    "amount": 4995,
    "currency": "usd",
    "reason": "fraudulent",
    "outcome": "won",
    "payment_link_id": null,
    "client_reference_id": null,
    "reference_id": "your-order-123",
    "customer": {
      "email": "jane@example.com",
      "name": "Jane Doe"
    },
    "closed_at": "2026-04-12T09:00:00Z"
  }
}
```

### checkout.expired

Fires when a checkout session passes its expiry without a completed payment — use it to release held inventory or trigger follow-up.

```json theme={null}
{
  "id": "evt_01YZA...",
  "type": "checkout.expired",
  "created_at": "2026-04-01T10:00:00Z",
  "data": {
    "checkout_id": "cs_abc123",
    "reference_id": "your-order-123",
    "payment_link_id": null,
    "client_reference_id": null,
    "customer": {
      "email": "jane@example.com",
      "name": "Jane Doe"
    },
    "amount": 4995,
    "currency": "usd",
    "created_at": "2026-03-17T10:00:00Z",
    "expired_at": "2026-04-01T10:00: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)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.