created_at if order matters.
Registering your endpoint
We send you messages at a URL you provide, with one URL per environment, and a signing secret which you can use to verify that the message came from Axle. To register your endpoint you can get in touch and provide the URL and secret for the events you want to receive. If you want different events to go to different endpoints we can do that too. We’ll first implement the webhook in the sandbox environment and once you’ve signed the integration off we’ll move it to production. You can change the URL or secret at any time, and we’ll start sending events to the new URL, and rotate the secret for you. Your endpoint only needs to accept HTTP POST requests with a JSON body, and return a2xx response once the event is durably accepted.
Events
The
vpp: events are for battery fleets on the Axle VPP, and carry a power setpoint in kW. asset:dispatch:requested is for EV charging, and carries a coarse action rather than a setpoint. An integration normally receives one family or the other, not both.The envelope
Every event shares the same envelope, with the event-specific fields inpayload, sent as Content-Type: application/json. We’ve formatted the example below for readability; on the wire it’s compact JSON, which matters once you get to verifying the signature.
Event payloads
Thepayload object contains the fields specific to the event type. The fields are stable for a given event, including across retries, but don’t rely on them matching the order documented here.
Field names aren’t shared across event types, so read the table for the event you’re handling rather than assuming a name carries over from another one.
user:onboarding:complete
asset:dispatch:requested
Asks you to drive an asset to a setpoint over a window. The event is sent when the dispatch is scheduled, which can be ahead of from_dt, so don’t act on it until the window opens. Return a 2xx if you will execute the dispatch, and a 4xx if you can’t: the asset is unknown, the window is invalid, or the asset is offline, for example.
We may add actions over time, so return a
4xx for any action you don’t recognise.
vpp:asset:registered
Fires when a customer finishes signup on Axle’s hosted VPP flow and we create the battery asset. It carries the identifiers you need for telemetry and dispatch, along with your own device serial so you can match the asset to your customer.
Return a 2xx once you’ve recorded the asset, and a 4xx only for a genuine refusal, such as a device serial you don’t recognise.
vpp:dispatch:requested
Asks you to hold a target power at each battery’s inverter over a window. One event covers every asset affected by a single grid event, so you get one delivery per event rather than one per battery.
We give you at least 30 minutes’ notice before an event, normally 4 hours to a day ahead. The event is sent when the dispatch is scheduled, so don’t act on it until start_time.
id as you would for any other event. Because the instruction is batched, that covers every asset in the event in one go.
power_kw is the target at the inverter. We normally dispatch at the inverter’s maximum power rating, and account for household load potentially limiting the power the battery delivers.We may add fields to a payload without changing
version, so ignore anything you don’t recognise rather than rejecting the request. We’ll version the envelope before we make a breaking change.Verifying the signature
Every request carries two headers:
Verify in four steps; both samples below do all of them.
1
Take the raw request body
Verify against the exact bytes we sent. Parsing the JSON and re-serialising it will change the
signature.
2
Parse x-axle-sig into its fields
Split on
,, then on the first =. Ignore fields you don’t recognise; we may add a second
signature scheme (v2=...) alongside v1 before retiring it.3
Reject stale timestamps
Discard the request if
t is more than 300 seconds from your clock. This is what bounds the
replay window.4
Recompute and compare
Compute the hex HMAC-SHA256 of
"{t}.{body}", keyed by your signing secret, and compare it to
v1 in constant time. Use the secret exactly as we gave it to you, as a UTF-8 string, without
decoding it first.- Python
- Node.js
Test vector
Check your implementation against this before you go live. Note the compact JSON, with no spaces between tokens.
Verifying that header against that body at a
received_at within 300 seconds of 1786456932 should succeed. Changing a single byte of the body, the secret or the timestamp should make it fail.
Responding
Return any2xx once you’ve durably accepted the event. We time an attempt out after 10 seconds, so persisting the event and processing it afterwards is usually easier than doing the work inline.
Retries
We retry a failed delivery with exponential backoff: 30 seconds after the first failure, doubling each time, capped at 4 hours, with ±10% jitter. Retries stop 24 hours after the event’screated_at, not 24 hours after the first attempt, so a delivery that starts late has a shorter window. Once that budget is spent we stop, and we won’t replay the event later.
Delivery guarantees
Delivery is at-least-once and unordered, so make your handler idempotent.
- Duplicates happen. A network failure after your endpoint has committed but before we see the response is indistinguishable from a failure, so we retry. Deduplicate on the envelope
id, which is stable across every attempt of the same event, rather than oncreated_ator payload contents. - Order isn’t guaranteed. Retries interleave with fresh events, so a later event can arrive before an earlier one. Where order matters, sequence on
created_atand treat an older event as a no-op if you’ve already applied a newer one. - Timing is best-effort. Events are normally delivered within seconds of
created_at, but there’s no latency guarantee. Don’t gate a user-facing flow on a webhook arriving promptly.
Quick checklist
- HTTPS endpoint live at the URL you gave us, one per environment.
-
x-axle-sigverified against the raw body before the event is acted on, and the test vector above passing. - Timestamps outside the 300-second tolerance rejected.
- Events deduplicated on the envelope
id. - Batched events handled per asset, with unrecognised
asset_ids skipped rather than refusing the whole instruction. -
2xxreturned only once the event is durably accepted,5xxon transient failure, and4xxonly for a genuine refusal. - Signing secrets stored as secrets, and kept distinct per environment.
Next steps
Smart Charging integration
Where onboarding fits in the wider integration
Axle VPP integration
Where the
vpp: events fit in a battery fleet integration
