Skip to main content
Webhooks push 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):
  • events lists the event types 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):
  • 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. 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 body:

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: 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 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 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 instead, with a realistic sample payload of that type:
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.

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:
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 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. 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.
Endpoints receive only events created after they were added. To give a new endpoint history, list /events and redeliver the ones you need.