# Webhooks

> Receive signed HTTPS notifications when things happen in your integration, instead of polling.

## What webhooks are for

Webhooks push notifications to an HTTPS endpoint you control whenever something happens in your integration that you care about. Each notification is delivered as a signed HTTP POST containing a JSON payload. Webhooks let you react in near real time without polling the REST API.

This page describes the parts of webhook delivery that are the same for every event: the envelope shape, the signature scheme, retries, idempotency, and ordering. The per-event payload (what `data` actually contains) is documented on the sub-page for each event type, and machine-readable in the [Webhooks reference](/docs/charge-now/reference/chargenow-api/webhooks).

## Available events

| Event type                                                            | Description                                                                        |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| [`session.updated`](/docs/charge-now/guides/webhooks/session-updated) | Fired whenever a charging session changes state or its energy/cost figures change. |

More event types will be introduced over time. Each one will be documented on its own sub-page.

## Getting set up

Webhook configuration is not self-serve. To start receiving events, contact us with:

1. **The HTTPS endpoint** that should receive deliveries.
2. **The event types** you want to subscribe to.
3. **A short description** (optional) so we can label the webhook in our system.

We will:

1. Create the webhook against your integration.
2. Generate a signing secret of the form `whsec_...` and share it with you over a secure channel.
3. Send a synthetic test event to your endpoint so you can verify the receiver before any real traffic flows. The synthetic event goes through the exact same delivery pipeline, signature, and retry policy as a production event.

Keep the secret somewhere your application can read it (a secrets manager, an env var). Anyone with the secret can forge a delivery, so treat it like an API key.

## The delivery envelope

ChargeNow delivers a [CloudEvents 1.0](https://github.com/cloudevents/spec/blob/v1.0/spec.md) envelope as the HTTP body. The shape is the same for every event; only `data` differs, carrying the event-specific payload. Field by field, the envelope and each payload are in the [Webhooks reference](/docs/charge-now/reference/chargenow-api/webhooks).

```
{
  "specversion": "1.0",
  "id": "5b9d6c1a-3d2f-5e88-9c1f-1a5b2e0e8e1a",
  "type": "session.updated",
  "source": "charge-now/api",
  "time": "2026-06-15T10:14:22.482931Z",
  "datacontenttype": "application/json",
  "data": { ...event-specific fields... }
}
```

Two fields matter for delivery handling: deduplicate on the top-level `id` (stable across retries, and the only id the signature covers), and order events for the same resource by `time` (a later `time` wins). New optional keys may appear over time; ignore unknown ones.

## Headers

```
Content-Type:      application/json
Webhook-Id:        <unique id per (webhook, event); stable across retries>
Webhook-Event:     <event type, e.g. session.updated>
Webhook-Timestamp: 1781234567
Webhook-Signature: 5f3b9c... (hex-encoded HMAC-SHA256)
```

The [reference](/docs/charge-now/reference/chargenow-api/webhooks) documents each header. Two matter here: `Webhook-Timestamp` and `Webhook-Signature` drive signature verification (below). `Webhook-Id` is handy for support requests but is not signed, so deduplicate on the envelope `id`, not on it.

## Verifying the signature

Always verify the signature **before** trusting any field in the body. The verification recipe is:

1. Read the raw request body as bytes. Do **not** parse and re-serialise it first, JSON formatting differences will break the signature.
2. Read the `Webhook-Timestamp` header.
3. Compute `HMAC-SHA256(secret, "<timestamp>.<raw-body>")` and hex-encode it.
4. Compare with the `Webhook-Signature` header using a constant-time comparison.
5. Reject the request if the comparison fails, or if the timestamp is older than a sensible window (we recommend 5 minutes).

Any language with a standard crypto library exposes the primitives you need: an HMAC-SHA256 implementation (e.g. `crypto/hmac` in Go, `crypto` in Node.js, `hmac` in Python, `javax.crypto.Mac` in Java) and a constant-time byte comparison (e.g. `hmac.Equal`, `crypto.timingSafeEqual`, `hmac.compare_digest`). Do not use a plain `==` on the digest, a naive comparison can leak the secret over time through timing side channels.

Two implementation details worth highlighting:

- **Capture the raw request body before any middleware parses it.** Most web frameworks expose a way to read the body as bytes (Express's `express.raw`, Go's `io.ReadAll(r.Body)`, Flask's `request.get_data()`, etc.). Parse the JSON only after the signature has verified.
- **Build the signing input as `"<timestamp>.<raw-body>"`** with a single ASCII `.` between the two parts. Feed it into HMAC-SHA256 keyed by your secret, hex-encode the digest, then compare.

## How to respond

| Your response                                                   | What ChargeNow does                                                                                                                                           |
| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `2xx`                                                           | Treats the delivery as successful and does not retry.                                                                                                         |
| `408 Request Timeout`, `425 Too Early`, `429 Too Many Requests` | Retries with exponential backoff.                                                                                                                             |
| Any other `4xx`                                                 | Stops retrying. The delivery is considered permanently failed. Use this for "I will never accept this" cases (e.g. signature mismatch is your bug, not ours). |
| `5xx`, connection error, TLS error, read timeout                | Retries with exponential backoff.                                                                                                                             |

Respond quickly. The HTTP call has a **30 second timeout per attempt**, after which we treat the attempt as failed and retry. Acknowledge the delivery first and process the event asynchronously (queue, background job) if your handler takes longer.

## Retries

Failed attempts are retried with exponential backoff and jitter, capped at 30 minutes between attempts, up to **25 attempts spread over roughly 9.5 hours**. The first few intervals are approximately:

```
5s, 15s, 45s, 2m15s, 6m45s, 20m15s, then 30m for the remaining attempts
```

After 25 attempts the delivery is dropped. There is no automated replay. If your endpoint was down for longer than the retry window, reconcile via the REST API.

## Idempotency and duplicates

ChargeNow guarantees **at-least-once** delivery. The same event may arrive more than once for legitimate reasons (a retry that completed on our side after a network blip, an upstream reprocess). Build your receiver to be idempotent.

The recommended strategy:

- Use the envelope `id` (top-level CloudEvents `id`, **not** the `Webhook-Id` header) as your deduplication key.
- The `id` is stable across all retries of the same logical event and is the only id protected by the signature.
- Track processed ids for at least the length of the retry window (10 hours is a safe minimum, 24 hours is comfortable).

## Ordering and race conditions

Webhooks are **not strictly ordered**. Two events for the same resource can arrive out of order, particularly if the first one had to be retried while the second went through on the first attempt. This is normal industry behaviour and not specific to ChargeNow.

Practical rules:

- **Treat each event as a snapshot, not a delta.** The `data` payload reflects the resource's known state at the time the event was produced.
- **Use the envelope `time` field to detect stale events.** If you already processed an event with a later `time` for the same resource, skip the older one.
- **Do not assume events arrive in the order they happened.** A later `time` always wins.

A robust handler in pseudocode:

```
on event:
    if seen(event.id): return 200
    record = load(resource_id from event.data)
    if record.last_event_time and record.last_event_time >= event.time:
        mark_seen(event.id)
        return 200
    apply(event.data)
    record.last_event_time = event.time
    mark_seen(event.id)
    return 200
```

Per-event-type ordering nuances (e.g. settlement corrections for `session.updated`) are documented on each event's sub-page.

## Reconciliation

Webhooks are an optimisation over polling, not a replacement for the source of truth. If correctness matters (billing, accounting, compliance), reconcile periodically by reading the underlying resource via the REST API. The REST response is authoritative, the webhook payload reflects the same data at the moment the event was produced.

## Disabling a webhook

To pause deliveries (e.g. during a deploy that touches your receiver), contact us and we will set the webhook to `disabled`. In-flight retries continue until the webhook is re-enabled or the retry window expires. To resume, contact us and we will set it back to `active`.

## Rotating the secret

To rotate the signing secret, contact us. We will create a new webhook with a fresh secret and disable the old one once you have cut over. Plan a brief overlap where your receiver accepts either secret during the cutover.

## Common pitfalls

- **Parsing the body before verifying the signature.** Always verify against the raw bytes. Any whitespace or key-order change breaks the HMAC.
- **Using non-constant-time comparison.** Use `hmac.compare_digest`, `crypto.timingSafeEqual`, or your language's equivalent. A naive `==` can leak the secret over time.
- **Assuming a single delivery per event.** Retries and at-least-once delivery mean duplicates happen, dedupe on the envelope `id`.
- **Assuming strict order.** Compare the envelope `time` against what you have already recorded.
- **Letting the handler block.** Any single attempt that exceeds 30 seconds is retried. Acknowledge fast, process async.
- **Trusting `Webhook-Id` for idempotency.** It is convenient for support requests but is not protected by the signature. Use the envelope `id`.

## Support

If a delivery is failing and you cannot tell why, send us the `Webhook-Id` header from a failed attempt (or the envelope `id`) and we can look up the delivery in our system.
