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

# Webhooks

> Receive signed events when things change in your fleet: setup, delivery, retries, ordering, redelivery and the event log.

Webhooks push [events](/webhooks/events) to your server as they happen: a session finished, the wallet was funded, a driver was added. They fire for changes made through **any** channel (the fleet portal, the driver app, the chargers themselves, and the API), not only for changes your own API calls made.

## Setting up an endpoint

Register an endpoint in the fleet portal under **Developers → Webhooks**, or with `POST /webhook_endpoints` (scope `webhooks:write`):

```bash theme={null}
curl https://partners.chargeexpressltd.com/api/public/v1/webhook_endpoints \
  -H "Authorization: Bearer $CHARGEEXPRESS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/chargeexpress",
    "description": "Production listener",
    "events": ["session.completed", "wallet.funded"]
  }'
```

* `events` lists the [event types](/webhooks/events) to send, or `["*"]` for all of them, including types added in future.
* The response includes the endpoint's signing `secret` (`whsec_…`). It is shown only on create and on rotate, so store it now.
* A fleet can have up to 10 endpoints in each environment. Change `url`, `events`, `description` or `status` (`enabled` / `disabled`) with `PATCH /webhook_endpoints/{endpointId}`.

### Sandbox and live endpoints

Endpoints belong to the environment of the key that registers them, like everything else (see [Authentication](/authentication#key-format)):

* An endpoint registered with a sandbox key (`cxk_test_…`), or under the sandbox view of **Developers → Webhooks** in the portal, receives only sandbox events.
* An endpoint registered with a live key (`cxk_live_…`) receives only live events.

Every event says which environment it came from in `livemode`: `true` for live, `false` for the [sandbox](/guides/sandbox). Sandbox and live endpoints, events and the event log are separate, so `GET /events` with a sandbox key lists only sandbox events. When you go live, register your endpoints again with a live key; they get new signing secrets. If you point both at the same URL, branch on `livemode`.

### URL requirements

ChargeExpress refuses URLs that could reach private networks:

* `https://` only, on port **443** or **8443**.
* No username or password in the URL.
* The host must resolve to public IP addresses. Private, loopback, link-local, carrier-grade NAT and cloud metadata ranges (IPv4 and IPv6) are refused, as are `localhost`, `*.local` and `*.internal`.

The check runs when you save the URL and again on every delivery, against the address actually used for the connection.

To develop locally, expose your machine through an HTTPS tunnel and register the tunnel's public URL.

## What a delivery looks like

Each delivery is an HTTP `POST` with a JSON [event](/webhooks/events) body:

```http theme={null}
POST /chargeexpress HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: ChargeExpress-Webhooks/1
X-ChargeExpress-Event-Id: evt_c4k8m2p6r0t4v8x2z6b0d4f8
X-ChargeExpress-Event-Type: session.completed
X-ChargeExpress-Signature: t=1791882200,v1=5f0c0d4c8b7c5b0f6c1b0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a
```

| Header | Description |
| - | - |
| `X-ChargeExpress-Signature` | The timestamp and HMAC signature(s). **Verify it** before trusting the body. See [Verifying signatures](/webhooks/verifying-signatures). |
| `X-ChargeExpress-Event-Id` | The event's `id`, also in the body. |
| `X-ChargeExpress-Event-Type` | The event's `type`, also in the body. |
| `User-Agent` | `ChargeExpress-Webhooks/1` |

## Responding

* Return any **2xx** status within **10 seconds** to acknowledge the delivery. The response body is ignored, apart from the first 1 KB being kept in the delivery log.
* Anything else is a failure: a non-2xx status, a timeout, a connection, DNS or TLS error, or a **redirect**. Redirects are never followed, so register the final URL.
* Acknowledge first and process afterwards. Put the event on a queue and return `200` straight away; slow handlers cause timeouts and duplicate deliveries.

## Retries

A failed delivery is retried with increasing delays. The first attempt is immediate; after a failure the next attempts wait:

| After attempt | Wait before the next attempt |
| - | - |
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 6 hours |
| 6 | 12 hours |
| 7 | 24 hours |

That is **8 attempts over about 45 hours**. If the 8th attempt fails, the delivery is marked `failed` and not retried automatically. You can still [redeliver](#redelivering-events) it.

Delivery attempts are made within a few seconds of their scheduled time.

## Disabled endpoints

If every delivery to an endpoint has failed for **3 days** in a row (counted from the first failure after its last success), ChargeExpress:

1. disables the endpoint (`status: "disabled"`, `disabled_reason: "failing"`),
2. marks its pending deliveries `failed`, and
3. emails the fleet's managers.

A single successful delivery resets the clock. Once you've fixed your server, re-enable the endpoint in the portal or with `PATCH /webhook_endpoints/{endpointId}` and `{"status": "enabled"}`. Re-enabling doesn't resend what was missed: use [redelivery](#redelivering-events) for that.

`failing_since` on the endpoint shows when the current run of failures started, so you can alert on it before the endpoint is disabled.

## Ordering and duplicates

Delivery is **at least once** and **unordered**:

* The same event can arrive more than once, for example after a timeout where your server did process the request. **Dedupe on the event `id`** (or the `X-ChargeExpress-Event-Id` header). Store ids you've processed for at least a few days.
* Events can arrive in a different order from the one they happened in, especially when retries are involved. A `session.completed` can arrive before its `session.started`.

To handle ordering:

* Use `created_at` to decide whether an event is newer than what you already have.
* Treat `data.object` as a snapshot of the object **when the event was created**. If you need the current state, fetch it (`GET /sessions/{sessionId}`, `GET /drivers/{driverId}`, …).
* Make handlers idempotent: applying the same `session.completed` twice should change nothing.

## Testing an endpoint

**Send test** in the portal, or `POST /webhook_endpoints/{endpointId}/test`, sends one signed event to the endpoint immediately and returns what your server answered (`event_id`, `type`, `succeeded`, `response_status`, `response_body`, `error`, `duration_ms`).

By default the event is a `ping`. Send `{"type": "<event type>"}` to send any [event type](/webhooks/events) instead, with a realistic sample payload of that type:

```bash theme={null}
curl -X POST https://partners.chargeexpressltd.com/api/public/v1/webhook_endpoints/we_m4n8p2q6r0s4t8u2v6w0x4y8/test \
  -H "Authorization: Bearer $CHARGEEXPRESS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "session.completed" }'
```

The sample is built the same way as a real event, so it has exactly the shape your handler will see, with `livemode` set for the endpoint's environment. Test events aren't stored, retried or listed in `/events`, so a test doesn't affect your reconciliation. To exercise the real sequence of events for a session or top-up, use the [sandbox](/guides/sandbox).

## Rotating the signing secret

`POST /webhook_endpoints/{endpointId}/rotate_secret` (or **Rotate secret** in the portal) returns a new `secret`. For the next **24 hours** ChargeExpress signs every delivery with **both** the new and the previous secret, so the signature header carries two `v1=` entries:

```text theme={null}
X-ChargeExpress-Signature: t=1791882200,v1=<signed with new secret>,v1=<signed with previous secret>
```

A receiver that checks "does any `v1` match my secret" keeps working throughout. Deploy the new secret within 24 hours; after that only the new secret signs. The [verification samples](/webhooks/verifying-signatures) accept several secrets, so you can also configure both during the switch.

## The event log

Every event (except test events) is stored for **30 days**, whether or not any endpoint was subscribed to it.

* `GET /events` lists them, newest first, [paginated](/pagination). Filter with `type` (comma-separated, for example `type=session.completed,wallet.funded`), `from` and `to`.
* `GET /events/{eventId}` returns one event with its `deliveries` (per endpoint: `status`, `attempts`, `next_attempt_at`, `last_response_status`, `last_response_body`, `last_error`) and a `delivery_summary`.

Use the event log to reconcile after an outage, or to backfill a new system.

## Redelivering events

`POST /events/{eventId}/redeliver` with `{"endpoint_id": "we_…"}` queues the event for that endpoint again, with a fresh set of 8 attempts. The endpoint must be enabled, or you get `409 conflict`. It works for any event in the last 30 days, including events the endpoint wasn't originally subscribed to, which makes it useful for backfilling.

A redelivery sends the same event, with the same `id` and content, so your deduplication will recognise it. The signature header is new.

> [!NOTE]
>
> Endpoints receive only events created after they were added. To give a new endpoint history, list `/events` and redeliver the ones you need.


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