<!-- canonical: https://docs.costplus.io/docs/guides/webhooks -->

> Receive real-time payment status notifications
Webhooks allow Cost+ to notify your server in real time whenever an order's status changes. Instead of polling the API, you provide a URL and Cost+ sends an HTTP POST request to it whenever there is an update.

## How Webhooks Work

1. You specify a `webhook_url` when creating an order, or configure a default webhook URL at the project level.
2. When the order status changes (e.g., from `new` to `completed`), Cost+ sends a POST request to your webhook URL.
3. Your server receives the webhook, then calls the Cost+ API to retrieve the full order details.
4. You update your system based on the verified order status.

> [!WARNING]
> Never trust the webhook payload alone to fulfill an order. Always verify the order status by making a **GET /v1/orders/\{id\}/** request after receiving a webhook. The webhook is a notification mechanism, not a source of truth.

## Webhook Payload

The webhook body is a JSON object containing the event type and the order ID:

```json
{
  "event": "status_changed",
  "order_id": "b9ae6...",
  "project_id": "proj_abc123"
}
```

| Field | Description |
| --- | --- |
| `event` | The type of event. Currently `status_changed` for order updates. |
| `order_id` | The unique identifier of the order whose status changed. |
| `project_id` | The project that the order belongs to. |

## Setting the Webhook URL

You can set the webhook URL in two ways:

### Per-Order

Include the `webhook_url` field when creating an order:

```json
{
  "currency": "EUR",
  "amount": 1295,
  "webhook_url": "https://www.example.com/webhook"
}
```

### Project Default

Configure a default webhook URL in your Cost+ dashboard under project settings. This URL will be used for all orders that do not specify their own `webhook_url`.

> [!TIP]
> Even if you have a project-level default, you can override it on a per-order basis by including `webhook_url` in the order creation request.

## Retry Logic

If your server does not respond with a `2xx` status code, Cost+ retries the webhook delivery:

- **Maximum retries:** 10 attempts
- **Retry interval:** 2 minutes between each attempt
- **First attempt timeout:** 4 seconds
- **Subsequent attempt timeout:** 10 seconds

If all 10 retry attempts fail, the webhook is marked as failed. You can still retrieve the order status at any time by polling the API.

> [!NOTE]
> Make sure your webhook endpoint responds quickly (within 4 seconds for the first attempt). Perform any long-running processing asynchronously after returning a `200 OK` response.

## Best Practices

- **Always verify via API.** Upon receiving a webhook, call `GET /v1/orders/\{order_id\}/` to confirm the current status before taking action such as shipping goods or granting access.
- **Return 200 quickly.** Acknowledge the webhook with a `200` response immediately and process the order asynchronously to avoid timeouts.
- **Handle duplicates.** Your webhook endpoint may receive the same event more than once. Ensure your processing logic is idempotent.
- **Use HTTPS.** Always use an HTTPS URL for your webhook endpoint to ensure the payload is transmitted securely.
- **Log webhook payloads.** Store incoming webhook data for debugging and audit purposes.

## Related Endpoints

- [Create Order](/api-reference/orders/createOrder) — set the `webhook_url` per order
- [Update Order](/api-reference/orders/updateOrder) — update the webhook URL on an existing order
- [Webhook Event Schemas](/api-reference/webhooks) — payload format for `OrderStatusChangedEvent` and `TransactionStatusChangedEvent`
