# Component token
Source: https://docs.axle.energy/api-reference/auth/component-token
POST /auth/component-token
Authenticate on behalf of an end user by supplying their external user ID.
This endpoint is used by authenticated API clients to create a user-scoped access token for a specific end user (referenced by their external ID) in the API client's organisation. If no such end user exists, one will be created on demand.
Returns a bearer access token scoped to this end user upon success.
This token is site-scoped and valid for 24 hours, longer-lived than the standard organisation token (1 hour) as it is intended for end-user sessions.
Component tokens are site-scoped and valid for **24 hours** — longer-lived than the standard [organisation token](./token-form) (1 hour), since they're intended for end-user sessions.
## Supplying `user_email`
A user email is required to process payments through our provider, Stripe. We also send payment-failure notifications and, if enabled, two-factor authentication codes to this address.
The field upserts onto the user record, so it's safe to set on every call.
# Authentication
Source: https://docs.axle.energy/api-reference/auth/token-form
POST /auth/token-form
Authenticate using username and password submitted via form data.
This endpoint is compatible with standard OAuth2 clients that use form-based authentication. Returns an access token upon successful authentication.
The returned token is valid for 1 hour, after which a new token must be requested.
Exchange your API credentials for a bearer token granting time-bound access to your organisation's resources.
This token is valid for **1 hour**. Once it expires you'll need to request a new one. For longer-lived, site-scoped access for end users, see the [Component token](./component-token).
# Send asset readings
Source: https://docs.axle.energy/api-reference/data/readings
POST /data/readings
Send asset readings data to Axle using the generic readings format.
This endpoint accepts compressed data using gzip encoding. Returns statuses of the processed readings, indicating whether each reading was accepted or rejected.
**Direction convention:** for readings that can be either positive or negative (e.g. `boundary_import_kw`, `battery_inverter_import_kw`), a positive value denotes *import* (power flowing in from the grid, or into the battery) and a negative value denotes *export* (power flowing out to the grid, or out of the battery). All other reading types are unidirectional; the required sign for each `label` is documented in the `ReadingDataPointLabel` schema below.
# Delete asset
Source: https://docs.axle.energy/api-reference/entities/asset/delete
DELETE /entities/asset/{asset_id}
Delete an asset by its Axle UUID.
The asset will be deleted if it exists. Returns a confirmation message upon successful deletion.
Use this endpoint when a device is physically removed from a site — for example, when a charger is decommissioned, replaced, or uninstalled.
**If a customer is opting out of the service, do not use this endpoint.** Call [unenrol](/api-reference/entities/site/unenrol) instead or in some specific situations use [Offboard site and assets](/api-reference/entities/site/offboard-site-and-assets)
# Send charge-now event
Source: https://docs.axle.energy/api-reference/entities/asset/event/charge-now
POST /entities/asset/{asset_id}/event/charge-now
Send notification of a charge now request for an asset.
# Send charge-now-deleted event
Source: https://docs.axle.energy/api-reference/entities/asset/event/charge-now-deleted
POST /entities/asset/{asset_id}/event/charge-now-deleted
Send notification of a charge now request being deleted for an asset.
# Send intent update event
Source: https://docs.axle.energy/api-reference/entities/asset/event/intent
POST /entities/asset/{asset_id}/event/intent
Send notification of an intent being updated on an asset.
# Send plug-in event
Source: https://docs.axle.energy/api-reference/entities/asset/event/plug-in
POST /entities/asset/{asset_id}/event/plug-in
Send notification of a car being plugged in.
Tariff, vehicle and intent information may be optionally provided.
If a tariff is included, it must comply with the OSM opening hours standard (https://wiki.openstreetmap.org/wiki/Key:opening_hours) for off-peak times and have a valid timezone, or a 400 error will be returned.
# Send plug-out event
Source: https://docs.axle.energy/api-reference/entities/asset/event/plug-out
POST /entities/asset/{asset_id}/event/plug-out
Send notification of a car being unplugged from an asset.
# Send asset event
Source: https://docs.axle.energy/api-reference/entities/asset/event/post
POST /entities/asset/{asset_id}/event
Send notification of an event happening on an asset.
This endpoint allows reporting various events related to an asset, such as:
- Plug-in/plug-out events
- Updates to charging intent
- Charge now events
If a tariff is included, it must comply with the OSM opening hours standard (https://wiki.openstreetmap.org/wiki/Key:opening_hours) for off-peak times and have a valid timezone, or a 400 error will be returned.
# Send tariff update event
Source: https://docs.axle.energy/api-reference/entities/asset/event/tariff
POST /entities/asset/{asset_id}/event/tariff-update
Send notification of a tariff update for an asset.
The `off_peak_times` field must comply with the OSM opening hours standard (https://wiki.openstreetmap.org/wiki/Key:opening_hours) and have a valid timezone, or a 400 error will be returned.
The same off-peak times apply every day, day-of-week selectors are not supported. Use `,` to separate multiple time ranges. Common examples:
- Same-day off-peak (e.g. midnight to 7am): `00:00-07:00`
- Overnight off-peak (e.g. 11:30pm to 5:30am): `00:00-05:30, 23:30-24:00`
# Get asset
Source: https://docs.axle.energy/api-reference/entities/asset/get
GET /entities/asset/{asset_id}
Get a specific asset by its Axle UUID.
Returns the asset details if it exists.
# Get asset by external id
Source: https://docs.axle.energy/api-reference/entities/asset/get-by-external-id
GET /entities/asset/external-id/{external_id}
Get a specific asset by its external ID (vendor-specific identifier).
Returns the asset details if it exists. This is useful for integrations where the external system ID is known but not the Axle UUID.
# Get all assets
Source: https://docs.axle.energy/api-reference/entities/asset/list
GET /entities/asset
Get all assets with pagination.
Returns a page of assets with the total count and the requested subset of asset records. Use the limit and offset parameters to navigate through pages of results.
# Create asset
Source: https://docs.axle.energy/api-reference/entities/asset/post
POST /entities/asset
Create a new asset.
The asset must be associated with an existing site. Returns the created asset details including the assigned UUID.
# Get asset price curve
Source: https://docs.axle.energy/api-reference/entities/asset/price-curve
GET /entities/asset/{asset_id}/price-curve
Half-hourly data of prices per MWh of flexed energy for the asset. Prices are returned from the current Settlement Period until 23:00 UK time on the same day or next day. Next day prices are available from 14:00 UK time.
# Update Asset
Source: https://docs.axle.energy/api-reference/entities/asset/update
PATCH /entities/asset/{asset_id}
Update an existing asset's properties, asset model, and/or installation date.
All fields are optional. Returns the updated asset details.
# Update event status
Source: https://docs.axle.energy/api-reference/entities/asset/update-status
POST /entities/asset/{asset_id}/callback/{event_id}
Provide an update on the handling of a given event by a given asset.
This endpoint allows asset controllers to report back the status of event handling. The `asset_id` can be either the Axle UUID or an external ID if `asset_id_is_external` is true.
Note: This will not work for non-UUID external asset IDs.
# Add site dispatch consent
Source: https://docs.axle.energy/api-reference/entities/site/add-consent
POST /entities/site/{site_id}/dispatch-consent
Add dispatch consent for a site to particpate in flex markets.
This enables the site to participate with the specified dispatch consent methods. Returns the updated site details with the new dispatch consent information.
# Add site boundary meter consent
Source: https://docs.axle.energy/api-reference/entities/site/add-meter-consent
POST /entities/site/{site_id}/meter-consent
Add boundary meter consent for a site.
Confirm that the site has consented for Axle to retrieve its boundary meter readings. This is required for participation in the wholesale market for assets that do not have CoP11 approval, but may also be provided via other endpoints (e.g. during site onboarding). Returns the updated site details with the new consent timestamp.
# Add site auth token
Source: https://docs.axle.energy/api-reference/entities/site/add-token
POST /entities/site/{site_id}/token
Provide an auth token to Axle, for dispatching the site's assets.
This token should provide access to all of the assets registered against the specified site.
This is only required where Axle is not able to access this site via an aggregated API in your systems.
# Delete site
Source: https://docs.axle.energy/api-reference/entities/site/delete
DELETE /entities/site/{site_id}
Delete an existing site by its UUID.
The site will only be deleted if it has no associated assets or other objects. Returns a confirmation message upon successful deletion.
# Check eligibility
Source: https://docs.axle.energy/api-reference/entities/site/eligibility
GET /entities/site/{site_id}/eligibility
Read-only eligibility breakdown for a site and its assets. Returns the list of propositions the site can enrol in, along with per-asset eligibility detail.
# Enrol site
Source: https://docs.axle.energy/api-reference/entities/site/enrol
POST /entities/site/{site_id}/enrol
Enrol a site in a proposition.
This is the commitment point — it activates the proposition for the site.
Idempotent: if the site is already enrolled, returns `already_enrolled` without making changes.
If the site previously withdrew from this proposition, returns 422. Set `override_withdrawal` to true to re-enrol.
# Get site flex events
Source: https://docs.axle.energy/api-reference/entities/site/flex-events
GET /entities/site/{site_id}/flex-events
Get info on flex events the site has participated in, including estimated and final gross revenues.
These estimates can be used for a variety of purposes, including paying out end-users before the final settlement data is available.However, to avoid overpayments it is recommended to only pay out a percentage of the estimated revenue initially, and top-up once final revenues are available.
Note: it is possible for gross revenues to be negative in some scenarios, e.g. if the site flexed in the wrong direction by consuming more energy while it was instructed to consume less.
# Get site
Source: https://docs.axle.energy/api-reference/entities/site/get
GET /entities/site/{site_id}
Retrieve a specific site by its UUID.
Returns the site details if it exists.
# Initialise site and assets
Source: https://docs.axle.energy/api-reference/entities/site/initialise
POST /entities/site/initialise
Initialise a site with one or more assets.
This endpoint upserts the site and assets (creating them if they don't exist, or filling in missing fields if they do), and returns eligibility information.
**No enrollment side effects**: Use the `/enrol` endpoint to commit to enrollment.
# Get all sites
Source: https://docs.axle.energy/api-reference/entities/site/list
GET /entities/site
Retrieve sites, optionally filtering by MPAN or postcode.
Returns a page of sites with the total count and the requested subset of site records. Use the limit and offset parameters to navigate through pages of results.
# Offboard site and assets
Source: https://docs.axle.energy/api-reference/entities/site/offboard
POST /entities/site/{site_id}/offboard-site-and-assets
Offboard a site and all its associated assets in a single operation.
Unenrols from every proposition, and deletes the site and assets in accordance with data retention policies.
Use when the user is leaving entirely — for example, they've moved house, or your support team is removing them. Payments can no longer be accessed afterwards.
If the user is only opting out of one proposition (e.g. toggling Flex Lite off in your app), use `unenrol` instead — it preserves the site and any other propositions they're enrolled in.
# Onboard
Source: https://docs.axle.energy/api-reference/entities/site/onboard
POST /entities/site/onboard
Initialise a site and its assets, and enrol them in a proposition in a single call.
This endpoint will:
1. Upsert the site and asset(s)
2. Enrol in the relevant flex proposition
The response returns the site and asset IDs you'll use in subsequent calls, and the enrolment outcome.
Onboarding is idempotent and atomic — sending the same site or asset returns the existing record with any new fields merged in; if enrolment fails, nothing is stored.
Already integrated against [`/onboard-site-and-asset`](./onboard-site-and-asset)?
It's still supported, but we'd recommend migrating to `/onboard` when convenient.
# Onboard site and asset (legacy)
Source: https://docs.axle.energy/api-reference/entities/site/onboard-site-and-asset
POST /entities/site/onboard-site-and-asset
**Deprecated** — prefer [`/onboard`](/api-reference/entities/site/onboard) instead.
The new `/onboard` endpoint supports multiple assets per site in a single call and includes improvements in idempotency, resilience and response details.
---
Register a site, asset, and dispatch consent in a single call.
Creates the site and asset, grants consent, and enrols in the relevant flex proposition.
This endpoint will:
1. Create the site
2. Create the asset associated with the site
3. Enrol in the relevant flex proposition
Returns the created site and asset, including their IDs. Fails if the site or asset already exists.
We'd recommend migrating to [`/onboard`](./onboard) — it supports multiple
assets per site, and a wider variety of common use cases. Existing integrations
against `/onboard-site-and-asset` will continue to be supported.
# Create site
Source: https://docs.axle.energy/api-reference/entities/site/post
POST /entities/site
Register a new site.
The site must have a valid address and postcode. If a site with the same address already exists, a 409 error will be returned with the existing site ID.
# Get site price curve
Source: https://docs.axle.energy/api-reference/entities/site/price-curve
GET /entities/site/{site_id}/price-curve
Half-hourly data of prices per MWh of flexed energy for the asset. Prices are returned from the current Settlement Period until 23:00 UK time on the same day or next day. Next day prices are available from 14:00 UK time.
# Remove site dispatch consent
Source: https://docs.axle.energy/api-reference/entities/site/remove-consent
POST /entities/site/{site_id}/dispatch-deconsent
Remove dispatch consent for a site to stop participation in flex markets.
This disables the site's participation with the specified dispatch consent methods. Returns the updated site details with the consent information removed.
# Unenrol site
Source: https://docs.axle.energy/api-reference/entities/site/unenrol
POST /entities/site/{site_id}/unenrol
Unenrol a site from a proposition.
Withdraws the site from the proposition and prevents further scheduling. Does not delete the site or its assets.
Use when the user is opting out of this proposition specifically — for example, they toggled it off in your app. The site and assets are kept, any other propositions they're enrolled in (such as Capacity Market) are unaffected, and the user can re-enrol later and still access their payments.
If the user is leaving entirely (moving house, support-led removal, etc.), use `offboard-site-and-assets` instead.
Idempotent: if the site is already unenrolled, returns `already_unenrolled` without making changes.
# Update site
Source: https://docs.axle.energy/api-reference/entities/site/update
PATCH /entities/site/{site_id}
Update an existing site's information.
Only fields included in the request will be updated. Returns the updated site details.
# Get example dispatch event
Source: https://docs.axle.energy/api-reference/examples/get-example-dispatch-event
GET /examples/dispatch-event
Get an example dispatch event.
Returns a pre-filled DispatchEvent object that can be used as a reference for the expected format and structure of dispatch events.
# Get example OCPP charging profile
Source: https://docs.axle.energy/api-reference/examples/get-example-ocpp-charging-profile
GET /examples/ocpp-charging-profile
Get an example OCPP charging profile.
Returns a pre-filled SetChargingProfileRequest object that can be used as a reference for the expected format and structure of OCPP charging profiles.
# Get meter info by address
Source: https://docs.axle.energy/api-reference/meter/get-by-address
POST /meter/by-address
Retrieve detailed information about a meter using its address.
This endpoint allows direct lookup of meter information by address, when the MPAN is not already known (if the MPAN is known, use the /meter/{mpan} endpoint).
# Get meter info by MPAN
Source: https://docs.axle.energy/api-reference/meter/get-by-mpan
POST /meter/by-mpan
Retrieve detailed information about a meter using its MPAN.
This endpoint allows direct lookup of meter information when the MPAN is already known.
# Get meter info by meter ref
Source: https://docs.axle.energy/api-reference/meter/get-by-ref
GET /meter/by-ref/{meter_ref}
Retrieve meter information using an encrypted reference.
# Search meters
Source: https://docs.axle.energy/api-reference/meter/search
GET /meter/search
Search for all electricity meters in the vicinity of a given postcode/ address.
This endpoint allows searching for meters by postcode and optionally by building identifier. For data protection and contractual reasons, meter details are returned in an encrypted format that can be used in subsequent API calls to retrieve the full meter information.
The search works in three steps:
1. Search for meters at the specified address using this endpoint
2. Ask the end-user to select the correct address from the list
3. Call the `/meter/by-ref/{meter_ref}` endpoint with the corresponding reference to get the full meter details
NB: This should not be used with procedural address matching; it is intended for end-user interaction only. For precise address matching, use the `/meter/by-address` endpoint instead, which will perform matching for you.
# Get Balance Info
Source: https://docs.axle.energy/api-reference/rewards/balance-info
GET /rewards/{site_id}/info
Get info about current balance and total earnings for the site. Requires an organisational bearer token.
# Get transaction history
Source: https://docs.axle.energy/api-reference/rewards/transactions
GET /rewards/{site_id}/transactions
Get history of transactions for the site. Requires an organisational bearer token.
# Get Balance Withdrawal URL
Source: https://docs.axle.energy/api-reference/rewards/withdrawal-url
GET /rewards/{site_id}/url/balance
Get a temporary link to an Axle-hosted form through which users can view and withdraw rewards. Requires an organisational bearer token.
**Important**: Anyone with access to this link will be able to withdraw funds from the corresponding site. The URL contains a JWT valid for 60 minutes from the time of generation. Use the `url_expiry_timestamp` field to surface or enforce this deadline client-side.
The ability to withdraw funds via API-generated links is disabled by default. [Contact us](mailto:support@axle.energy) to enable it for your organisation.
# Setting up an environment
Source: https://docs.axle.energy/api-reference/sandbox
Get started with a sandbox environment
# Environments
Axle provides **sandbox** environments for testing and development purposes. The API docs are interactive; once you're authenticated, you will be able to make
calls to `api-sandbox.axle.energy` via these docs.
Please [get in touch](mailto:hello@axle.energy) to setup a new sandbox account.
When you're ready to roll with a **production** environment, please [get in touch with the team](mailto:hello@axle.energy).
## Use your credentials to obtain a token
We use token-based authentication, whereby you exchange your API credentials for a token which gives
time bound access to specific resources.
To get a token for your session, see the [Authentication](../api-reference/auth/token-form) page, using the `username` and `password`
provided when you created your new sandbox.
This token must be included in subsequent requests as a bearer token:
```bash theme={null}
'Authorization': 'Bearer '
```
This token expires after 1 hour. You'll need to refresh it after that.
# Validate an MPAN
Source: https://docs.axle.energy/api-reference/validation/mpan
POST /validation/mpan
Validates the provided MPAN as one that is well-formatted and is associated with a connected import meter
# Batteries
Source: https://docs.axle.energy/assets/batteries
Axle has integrated with a wide array of domestic and commercial battery systems. We've got a
track record of operating large fleets of batteries in [demand response](https://www.axle.energy/blog/dfs-case-study) and
constraint management.
We're also the only provider of [frequency services to the UK grid](https://www.axle.energy/blog/frequency) using domestic batteries. Notably,
we do this without any additional hardware, utilizing existing cloud APIs to provide swift and stable responses of several MW.
# Integrating with Axle
You can either integrate into the Axle API, or we'll integrate into an existing cloud API.
### Telemetry
We typically gather:
* State of charge (SOC) `%`
* Inverter Power `kW`
* Grid Power `kW`
* Solar Power `kW`
You can push these to our [Asset Readings](../api-reference/data/readings) endpoint.
### Dispatch
You can register for either Delegated Control or Event-based dispatch as described [here](../tutorial/dispatch#types-of-dispatch)
### Telemetry
We typically gather:
* State of charge (SOC) `%`
* Inverter Power `kW`
* Grid Power `kW`
* Solar Power `kW`
We can retrieve these via polling or webhook.
### Dispatch
We have extensive experience integrating with both event-based or schedule-based APIs.
## Next steps
Optimize behind-the-meter batteries
Powerful APIs for advanced integrations
# Cars
Source: https://docs.axle.energy/assets/cars
Axle integrates directly with electric vehicles via your API, enabling smart charging control without requiring a connection to your charger.
Your customers can participate in flexibility programmes regardless of which charger they use at home.
## Smart charging principles
We always prioritise the customer's needs when optimising car charging.
Our optimisation satisfies the following constraints, in order:
1. Ensuring the customer's desired State of Charge (SOC) is met
2. Minimising customer's bills
3. Maximising flexibility revenue
## Customer propositions
### The "free miles" proposition
Car integrations are particularly well-suited to **free miles** propositions — where Axle pays the customer per kWh charged, offsetting their electricity costs. The cost of charging is calculated at a flat fee per kWh (e.g. 8p/kWh), and is paid by Axle to the customer's bank account.
1. Customers opt-in to smart charging, delivered via your app
2. Axle schedules charging via a backend integration, ensuring customer's needs are met whilst maximising flexibility value.
3. Axle pays customers per kWh of charging, offsetting their electricity costs.
### Custom propositions
Axle's flexible payment rails allow you to build custom propositions for your customers, mixing up-front and recurring payments. See [paying users](/workflows/ev-charging/paying-users).
## Geofencing
Axle only controls charging when the vehicle is at home. To determine this, we use a **"car is home" signal from your API** — we do not require location or GPS data, only a boolean indicating whether the car is at home.
In practice, this often means:
* Your API sends plug-in events to Axle only when the car is at home
* Plug-in events at public or workplace chargers should not be forwarded to Axle
Axle treats every plug-in event it receives as a home charge, so the simplest implementation is to filter at source.
## Next steps
Choose your EV flexibility solution and start planning your integration [here](/workflows/ev-charging/overview).
# Chargers
Source: https://docs.axle.energy/assets/chargers
Axle works with the world's most successful EV charger brands to build flexibility
into their charging services.
Chargers typically offer a lower-latency, higher-stability connection than vehicles, allowing
them to participate in more stringent flexibilty markets such as Frequency Response.
Axle also brings extensive experience in hardware and protocol qualification for chargers. We were
the first to participate in the UK's [Capacity Market](https://www.axle.energy/blog/capacity-market) directly
with EV chargers, and can advise and co-ordinate testing for protocols such as CoP11.
[Asset Metering](../tutorial/telemetry) can unlock more value in the wholesale market. In the future, asset metering
will likely also allow increased participation in the wholesale market, boosting eligibility from 25% to 100%.
Code of Practice 11 (CoP11) is the qualification necessary to use Asset Metering in the wholesale market.
CoP11 requires the testing of 2 sample devices. This test qualifies all devices with an equivalent hardware. The qualification process consists of 2 parts: (1) Compliance Testing; (2) Protocol Testing. Qualification for CoP 1, 2, 3, 5 or 10 exempts the need for Compliance Testing.
**Compliance Testing**
Compliance Testing verifies the meter hardware’s accuracy (+/-2%) and functionality. This process is expected to take 3-4 months, mostly driven by third-party testing turnaround times. Testing is only required for two sample devices - factory testing of all units is not required.
**Protocol Testing**
Protocol Testing ensures that the meter can be accessed by an Asset Metering Half-Hourly Data Collector, such as Axle. The process should take \~2 weeks to complete.
Axle can help arrange compliance testing, and run protocol testing. [Get in touch](mailto:alice@axle.energy) to discuss how we can get your assets CoP11 certified.
## Next steps
Choose your EV flexibility solution and start planning your integration [here](/workflows/ev-charging/overview).
# HVAC
Source: https://docs.axle.energy/assets/heating
Axle integrates with electric heating and cooling systems to deliver thermal comfort at
the minimum cost.
# Control via thermostat
We can either control the power of heating & cooling systems directly, or by using a set-level approach via
a smart thermostat. Indirect control via the thermostat allows you to ensure that the dynamics of control
remain within manufacturer's intended bounds. This can allay concerns around reduced operating efficiency (sCOP),
particularly of heat pumps.
We use Enode to connect to thermostats - see [their list of support hardware](https://developers.enode.com/api/capabilities/hvac).
# Integrating with Axle
You can either integrate into the Axle API, or we'll integrate into an existing cloud API.
### Telemetry
If we are integrating directly with the heating system we'll need:
* Power `kW`
* Temperature `°C`
* Outside temperature `°C` *Optional*
Pushed to our [Asset Readings](../api-reference/data/) endpoint.
If we're optimizing via the thermostat, temperature alone suffices.
### Dispatch
You can register for either Delegated Control or Event-based dispatch as described [here](../tutorial/dispatch#types-of-dispatch).
### Telemetry
If we are integrating directly with the heating system we'll need:
* Power `kW`
* Temperature `°C`
* Outside temperature `°C` *Optional*
If we're optimizing via the thermostat, temperature alone suffices.
We can retrieve these via polling or webhook.
### Dispatch
We'll either need to control power or temperature, if you're integrating [via thermostat](#control-via-thermostat).
## Next steps
Get started dispatching assets
Powerful APIs for advanced integrations
# Address Collection
Source: https://docs.axle.energy/components/components/address
# Details
Accurate address information is essential for participation in flex markets. We use
the address to retrieve the meter identifier for the household, which is used
to register the site in flex markets.
# How to use
This component can be embedded as a React component or an iFrame, and styled to match your brand.
See the [component playground](https://components.axle.energy/) for full details.
# Smart charging intent
Source: https://docs.axle.energy/components/components/smart-charging
# Details
Use the smart charging intent component to understand user requirements
for smart charging.
The component can be customised to collect charge requirements in different
forms:
* kWh (pictured above)
* Hours of charging
* % Charging
* % Target Charge
See the [smart-charging integration guide](../../workflows/smart-charging) for full details.
# How to use
This component can be embedded as a React component or an iFrame, and styled to match your brand.
See the [component playground](https://components.axle.energy/) for full details.
# Tariff
Source: https://docs.axle.energy/components/components/tariff
# Details
Use the tariff component to collect details of the user's tariff.
We gather the minimum amount of tariff data possible, to minimise user dropoff.
If your usecase requires more detailed tariff information (e.g. £/kWh, export rates), we can provide this
via a managed [FlatPeak](https://docs.flatpeak.com/start/connect/explore) integration.
# How to use
This component can be embedded as a React component or an iFrame, and styled to match your brand.
See the [component playground](https://components.axle.energy/) for full details.
# Smart charging app
Source: https://docs.axle.energy/components/getting-started/hosted-app
Axle provides a complete hosted smart-charging app that can be added to your existing application as its own page.
With this solution, Axle manages the entire smart-charging flow end-to-end.
## Embedding the app
You can either link directly out to `https://smart-charging-sandbox.axle.energy`, or embed the page within a Native app
using a WebView.
Axle will manage onboarding users and controlling their charging.
To connect to a users charger and car, Axle can integrate with your APIs, or Axle also works with Enode to gain
programmatic access to assets.
To see what including Enode in your onboarding flow might look like, see [Integrating with native Enode Link SDK](/components/guides/enode-integration)
## Theming
The app can be styled to match your brand. See the [theming guide](/components/guides/theming) for the full set of design tokens available.
## Authentication
### IFrame
To securely pass tokens into the smart charging application when hosting in an IFrame, use the `postMessage` API.
```
iframe.window.postMessage(
{
type: "axle:token",
token: "",
},
"https://smart-charging-sandbox.axle.energy"
);
```
### Webview
If hosting the Smart charging application within a native application, you can pass in the token as a header in the initial request to `https://smart-charging-sandbox.axle.energy/auth/token`.
This page will securely establish a session, then redirect the customer to the appropriate page (a dashboard or proposition screen)
The Axle hosted app will manage further authentication to Axle APIs.
This example is for iOS, but the approach is similar on Android. If you want integration advice, [get in touch](mailto:support@axle.energy) and we'll happily provide technical guidance
```swift theme={null}
final class ViewController: UIViewController {
private let config = WKWebViewConfiguration()
private lazy var webView: WKWebView = {
return WKWebView(frame: .zero, configuration: config)
}()
override func viewDidLoad() {
super.viewDidLoad()
view = webView
let url = URL(string: "https://smart-charging-sandbox.axle.energy/auth/token")!
var req = URLRequest(url: url)
req.setValue(token, forHTTPHeaderField: "token")
webView.load(req) // Header goes out on this initial request only
}
private let token = "your-one-time-token"
}
```
# Hosted Components
Source: https://docs.axle.energy/components/getting-started/hosted-components
Axle provides fully-hosted components that can be embedded in your application through iFrames or mobile web views. These components are served from Axle's infrastructure and communicate with your application through URL parameters and postMessage events.
### Communication
Hosted components communicate with your application through:
* **URL Parameters**: Pass configuration and theming data through query parameters
* **PostMessage Events**: Send and receive form lifecycle events through the `postMessage` API.
### Authentication
To prevent JWT tokens from being logged or cached in URLs, these must be sent through the postMessage API.
**To ensure security, we validate the origin of all messages received in iFrames against the origin specified
in the JWT.** This means you must embed the iFrame within a website hosted on your specified `allowed_origin`.
You must specify the **destination origin** in `postMessage` to prevent leaking tokens to untrusted websites
```js theme={null}
iframe.contentWindow.postMessage({
token: jwtToken,
type: 'axle:token'
}, 'https://app.axle.energy'); // MUST specify this
```
### Theming
Hosted components support comprehensive theming through URL parameters. You can customize the appearance by passing color and styling properties in the URL query string.
#### Color Properties
Pass color values using these URL parameters:
* `background` - Main background color
* `foreground` - Primary text color
* `popover` - Popover background color
* `popoverForeground` - Popover text color
* `primary` - Primary brand color
* `primaryForeground` - Primary button text color
* `secondary` - Secondary background color
* `secondaryForeground` - Secondary text color
* `destructive` - Error/destructive action color
* `border` - Border color
* `ring` - Focus ring color
* `muted` - Muted background color
* `mutedForeground` - Muted text color
#### Dark Mode Support
For dark mode theming, prefix any color property with `dark-`:
* `dark-background` - Dark mode background color
* `dark-foreground` - Dark mode text color
* `dark-primary` - Dark mode primary color
* etc.
#### Additional Properties
* `radius` - Border radius (e.g., `0.5rem`, `8px`)
* `fontFamily` - Font family name
#### Example Usage
```html theme={null}
```
All color values must be valid CSS colors (hex, rgb, rgba, hsl, hsla, oklch) and size values must include valid CSS units.
# Introduction and Authentication
Source: https://docs.axle.energy/components/getting-started/introduction
Components are fully-integrated, customisable UI building blocks that can be used
to build smart charging experiences within your app. They're built on top of Axle's
existing API and allow your customers to onboard their smart assets easily. Axle also provides
a pre-built "best-practice" smart charging app, which is designed to be embedded as a tab or seperate
page in an existing app.
The components are open source and built using React. You can either use them directly,
or use our fully-hosted option and embed Components in your app through iFrame or mobile
web views. Both options provide full theming customisation, and you can pick and choose
which components to use.
The smart charging app is available as a hosted option only, as Axle manages the whole customer
integration for you.
## Choosing a rendering option
| | React components | Hosted components | Smart charging app |
| --------------------------------- | ---------------------------------------- | ------------------------------------------ | ----------------------------------------------- |
| **Customisable theming** | ✅ | ✅ | ✅ |
| **Plug and play** | ❌ - Use in existing React website | ❌ - You pick the components to use | ✅ - Axle provides the whole smart-charging flow |
| **Build my own flow** | ✅ - Use what you need in an existing app | ✅ - Embed what you need in an existing app | ❌ |
| **Enhance an existing React app** | ✅ - Drop in Axle React components | ❌ | ❌ |
## Authentication
Regardless of how you choose to render, you'll need to obtain an end-user scoped component
token that can be used to authenticate API calls made from the frontend. This is a token
scoped to a specific user for use with components only, and must be created using your org scoped token.
To get your org scoped auth token, follow the [authentication guide](../../api-reference/auth/token-form). **Never** use your org scoped token in an untrusted environment, including all client side code.
To generate an end user scoped token, you'll need to specify an `external_user_id` and `allowed_origin`. These are required
so Axle can reconcile your end users with the entries in our database, and to ensure that redirects or iFrame embeddings
can only happen on domains you control.
```mermaid theme={null}
sequenceDiagram
actor user as End User
participant be as Your Backend
participant axle as Axle
user->>be: Authenticates with you using your existing auth flow
be->>axle: Get component scoped token
Note over axle: Axle creates a Site entity using provided external_user_id
axle->>be: Return end user scoped token
be->>user: Return end user scoped token to frontend
user<<->>axle: Frontend components talk directly to Axle using scoped token
```
You can obtain a component token by calling the `/auth/component-token/` endpoint:
```bash Request theme={null}
curl -X 'POST' \
'https://api.axle.energy/auth/component-token' \
-H 'accept: application/json' \
-H 'Authorization: Bearer ' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'external_user_id=&allowed_origin='
```
```json Response theme={null}
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30" }
```
### Refreshing tokens
Component tokens are valid for **one hour**. You should regenerate component tokens before this to ensure
user flows aren't interrupted.
# React Components
Source: https://docs.axle.energy/components/getting-started/react-components
Axle publishes an NPM package (`@axle-energy/components`) which can be imported into your existing React project.
These components are pre-configured to make authenticated API calls to Axle, and expose complete theming control
through CSS variables, or a helper React context.
```tsx Example integration theme={null}
import { TariffForm, IntentForm } from "@axle-energy/component/form";
import { UserInfoProvider } from "@axle-energy/core";
function MyApp() {
return (
console.log("Tariff saved:", data)}
onError={(error) => console.error("Error:", error)}
/>
);
}
```
## Theming
You can theme the components by either:
* **Setting CSS variables**. Components use standard ShadCN/UI theming, so you're able to theme our components by setting the default [ShadCN/UI theming variables](https://ui.shadcn.com/docs/theming#list-of-variables).
* **Using our `ColorThemeContextProvider` helper**. Axle also exposes a theming provider (which also works by setting CSS variables), with light/dark mode theming support:
```tsx Theming example theme={null}
function MyApp() {
const theme = {
radius: "0.625rem",
background: "#f0f0f0",
foreground: "#000000",
popover: "#ffffff",
popoverForeground: "#000000",
primary: "#007AFF",
primaryForeground: "#ffffff",
secondary: "#f2f2f7",
secondaryForeground: "#000000",
destructive: "#FF3B30",
border: "#c6c6c8",
ring: "#007AFF",
muted: "#f2f2f7",
mutedForeground: "#6d6d70",
fontFamily: "sans-serif",
}
return
// Components...
}
```
# Integrating with native Enode Link SDK
Source: https://docs.axle.energy/components/guides/enode-integration
Axle works with [Enode](https://enode.com/) to provide access to a wide array of EVs, chargers, heat pumps, and batteries.
Axle will call the Enode API directly on your behalf to read from and control assets. We provide a pre-built integration
to help you gather credentials from your users, access assets, and confer the ability to control them to Axle.
Enode host their own onboarding flow - [LinkUI](https://developers.enode.com/docs/link-ui/introduction). This guide
will demonstrate how to use Axle Components and LinkUI to onboard and connect an asset.
If you're developing in another platform, [get in touch](mailto:support@axle.energy) to discuss how we can provide a native integration.
## Step 1: Render the Axle Enode component
This is a helper component, which will automatically generate and return correctly
scoped Enode tokens.
The end user will see a loading indicator, and you'll need to listen to messages to correctly
show the Native Enode SDK.
```swift Example component view theme={null}
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let url: String
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
guard let url = URL(string: url) else { return }
let request = URLRequest(url: url)
webView.load(request)
}
}
struct AxleEnodeIntegration: View {
var body: some View {
NavigationView {
WebView(url: "https://app.axle.energy/form/enode/vehicle")
}
}
}
```
# Step 2: Listen for the Enode token from the component
You'll need to listen for the Enode token once the component has authenticated and fetched from Axle. The Enode
component automatically sends this token using `WKWebView` message handlers on iOS.
We'll first register the message handlers in the WebView. This example is using SwiftUI,
but it's also easy to do this in UIKit by implementing the `WKScriptMessageHandler` delegate.
```swift Register message handler in the WebView highlight={3-4, 8-11, 16-32} theme={null}
// In EnodeWebView struct
// Two way communication with parent view
@Binding var messageHandler: ((String, Any?) -> Void)?
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
// Register a message listener in the webview
let userContentController = webView.configuration.userContentController
userContentController.add(context.coordinator, name: "axleEnode")
return webView
}
// Called automatically by SwiftUI when instantiating the view
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, WKScriptMessageHandler {
var parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
parent.messageHandler?(message.name, message.body)
}
}
```
```swift Adding a JS listener lines highlight={2, 6, 8-10, 13-26} theme={null}
struct AxleEnodeIntegration: View {
@State private var messageHandler: ((String, Any?) -> Void)?
var body: some View {
NavigationView {
WebView(url: "https://app.axle.energy/form/enode/vehicle", messageHandler: $messageHandler)
}
.onAppear {
setupMessageHandler()
}
}
private func setupMessageHandler() {
messageHandler = { messageName, messageBody in
DispatchQueue.main.async {
switch messageName {
case "axle:enode-handoff":
handleSuccess(data: messageBody)
default:
print("Unknown message: \(messageName)")
}
}
}
}
}
```
## Step 3: Handle the Enode token and trigger LinkSDK
Once you receive the Enode token from the Axle component, you'll need to present the LinkSDK to complete the asset connection. First, import LinkKit and add the necessary state variables.
```swift Import LinkKit and add state variables highlight={2, 5-6} theme={null}
import SwiftUI
import LinkKit
struct AxleEnodeIntegration: View {
@State private var isLinkKitPresented = false
@State private var linkToken: String? = nil
@State private var messageHandler: ((String, Any?) -> Void)?
// ... rest of the view
}
```
Next, update your message handler to extract the link token from the response and trigger the LinkSDK:
```swift Handle token and trigger LinkSDK highlight={6-11} theme={null}
private func setupMessageHandler() {
messageHandler = { messageName, messageBody in
DispatchQueue.main.async {
switch messageName {
case "handoff":
// Extract link token from the response
if let responseData = messageBody as? [String: Any],
let token = responseData["linkToken"] as? String {
linkToken = token
isLinkKitPresented = true
}
default:
print("Unknown message: \(messageName)")
}
}
}
}
```
Finally, add the LinkKit sheet to your view and handle the results:
```swift Add LinkKit sheet highlight={8-16} theme={null}
var body: some View {
NavigationView {
WebView(url: "https://app.axle.energy/form/enode/vehicle", messageHandler: $messageHandler)
}
.onAppear {
setupMessageHandler()
}
.linkKitSheet(isPresented: $isLinkKitPresented, linkToken: linkToken) { linkResult in
switch linkResult {
case .success(let success):
handleLinkSuccess(success)
case .failure(let error):
handleLinkError(error)
}
}
}
```
Add the success and error handlers:
```swift Handle LinkSDK results theme={null}
private func handleLinkSuccess(_ success: LinkSuccessMetadata) {
print("LinkKit success: \(success)")
// Move to the next stage in your onboarding!
}
private func handleLinkError(_ error: LinkError) {
print("LinkKit error: \(error)")
// Handle LinkKit error
// Show error message to user or retry flow
}
```
# Theming
Source: https://docs.axle.energy/components/guides/theming
Apply per-partner branding to the smart charging experience using CSS variables.
The embed exposes a number of design tokens which we can customise to suit the branding needs of your application. Under the hood, these are set as CSS variables and piped into Tailwind CSS for use in our components.
To create or edit your theme, please reach out to an Axle representative. For new clients, we'll action this as part of your onboarding.
## Design tokens
All tokens are optional. Unset tokens fall back to our default theme.
### Colour tokens
Colours are expressed as CSS colour strings. The codebase prefers [OKLCH](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch) for perceptual uniformity, but hex and HSL are also supported.
Tokens are organised into base/foreground pairs. The base token (e.g. `--primary`) controls a surface colour, and its matching `-foreground` token (e.g. `--primary-foreground`) controls the text and icon colour that sits on that surface. Set them together so that content remains legible against each surface.
#### Brand
| CSS variable | Purpose | Default | |
| ------------------------ | -------------------------------------- | ------------------ | -------- |
| `--primary` | Main brand colour — buttons, key links | `oklch(0.205 0 0)` | |
| `--primary-foreground` | On primary surfaces | `oklch(0.985 0 0)` | |
| `--secondary` | Secondary brand colour | `#2e2f35` | |
| `--secondary-foreground` | On secondary surfaces | `#e5e5e7` | |
| `--accent` | Accent colour for emphasis | `#2e2f35` | |
#### Surfaces
| CSS variable | Purpose | Default | |
| ---------------------- | ------------------------------------------ | ------------------ | -------- |
| `--background` | Page or view background | `oklch(1 0 0)` | |
| `--foreground` | Default body text | `oklch(0.145 0 0)` | |
| `--card` | Card surfaces | `oklch(0.99 0 0)` | |
| `--popover` | Popover / dropdown background | `oklch(1 0 0)` | |
| `--popover-foreground` | On popover surfaces | `oklch(0.145 0 0)` | |
| `--muted` | Subdued backgrounds — inputs, badges | `#f3f2f280` | |
| `--muted-foreground` | On muted surfaces | `oklch(0.556 0 0)` | |
| `--scrim` | Backdrop overlay behind sheets and dialogs | `oklch(0.975 0 0)` | |
| `--pill` | Pill / chip backgrounds | `oklch(1 0 0)` | |
| `--pill-foreground` | On pill surfaces | `oklch(0.205 0 0)` | |
#### Feedback
| CSS variable | Purpose | Default | |
| ---------------------- | ------------------------------ | --------------------------- | -------- |
| `--destructive` | Error / destructive actions | `oklch(0.577 0.245 27.325)` | |
| `--warning` | Warning alert background | `rgba(254, 250, 231, 1)` | |
| `--warning-foreground` | On warning surfaces | `rgba(72, 61, 4, 1)` | |
| `--info` | Informational alert background | `rgba(235, 243, 250, 1)` | |
| `--info-foreground` | On info surfaces | `rgba(27, 70, 101, 1)` | |
#### Borders & focus
| CSS variable | Purpose | Default | |
| ------------ | --------------- | ------------------ | -------- |
| `--border` | Default borders | `oklch(0.922 0 0)` | |
| `--ring` | Focus ring | `oklch(0.708 0 0)` | |
### Radius
A single `--radius` value drives the entire corner-radius scale. The derived values `--radius-sm`, `--radius-md`, `--radius-lg`, `--radius-xl`, and `--radius-2xl` are all calculated from `--radius`, so components scale proportionally — set the base value and everything from inputs to cards to sheets stays in tune.
| CSS variable | Purpose | Default |
| ------------ | ------------------ | -------- |
| `--radius` | Base corner radius | `1.2rem` |
### Typography
| CSS variable | Purpose | Default |
| ------------------------ | --------------------------------------------------- | ------- |
| *(inline `font-family`)* | Body text font stack, applied via an inline `style` | `Inter` |
| `--font-heading` | Heading font stack, applied to `h1`–`h6` | `Inter` |
## Font customisation
We support three font strategies.
### 1. External fonts
We support the use of [Google Fonts](https://fonts.google.com/) for externally hosted, publicly licensed fonts.
### 2. Local fonts
We support the use of custom fonts. You can provide us with font files, which we host alongside the embed.
Themes that rely on brand fonts typically use this path, with one or more weights defined and `preload` enabled for primary weights.
When using custom fonts, please ensure you have the required licenses for use in the app.
### 3. System fonts
Themes may also choose not to ship any font files, instead using a system font stack.
## Copy and content configuration
Various parts of the onboarding flow are configurable per partner requirements. This is handled as part of dedicated engineering effort as we support your integration.
These theming options should cover the majority of needs for the majority of clients. If you have further requirements, please reach out to us via your customer support contact.
# Participating in flexibility markets
Source: https://docs.axle.energy/flex-lifecycle
The lifecycle of a flexibility asset
Although markets vary in their details, there are a number of common steps that most assets will go through. Axle manages
this process on your behalf, from registering assets through to revenue.
Typically the market operator will require the smart meter identifier (the [MPAN](https://smartmetercheck.citizensadvice.org.uk/meters/new?fuel_type=electricity) in the UK) and the asset type for us to register the asset. In some cases
we'll need to know more, such as the asset's max power or capacity.
There's typically a delay after an asset is registered before it can participate in the market. This is usually a few days, but can be longer in some cases.
Axle will determine the optimal bidding strategy for your asset, based on the market rules and the asset's capabilities.
Markets operate on different timescales. In some cases we'll be placing bids for the next day, in others we'll be placing bids weeks or months in advance.
We'll deliver the contracted flexibility to the market operator by dispatching your assets, and collecting the requisite telemetry data.
After a dispatch, we'll submit metering data to the market operator and collect the payment. We are typically able to
estimate our earnings within 24hrs of a dispatch, but the final settlement can take weeks or months, depending on the market.
# Access flexibility markets with ease
Source: https://docs.axle.energy/introduction
Register and prequalify assets, participate in flexibility markets, and monetize.
Axle provides access to flexibility markets across Europe. Building on the Axle platform, you can craft user experiences around flex, register and prequalify your assets, and monetize your flexibility.
You can find an overview of our currently supported markets in the [Markets](/markets) section.
# What is flexibility?
Flexibility is the ability to adjust energy consumption or generation in response to changing conditions. This can include shifting energy use to off-peak times, reducing demand during peak periods, or increasing generation when demand is high.
Axle helps you realize the latent flexibility of assets you control, by providing components and hosted flows that you can use
to sign up users and understand the flexibility in their energy usage. Axle will then help you monetize that flexibility by participating in flexibility markets on their behalf, by suggesting the
optimal scheduling of those assets, and by monetizing the flexibility accordingly.
## Support
Please [reach out](mailto:support@axle.energy) if there's anything we can help with.
## Where next?
Learn more about [participating in markets](./flex-lifecycle) or view specific integration guides for [EV chargers](./workflows/ev-charging/overview) or [batteries](./workflows/axle-vpp/overview).
# Denmark
Source: https://docs.axle.energy/markets/denmark
### Overview
Denmark is split into 2 zones: DK1 (West) is closely aligned with EU balancing arrangements, and DK2 (East) is integrated into the common Nordic balancing zone. The BSP role is available without BRP consent for FCR in DK1 and FFR and FCR-D in DK2.
In DK2, the asymmetric FCR-D (Disturbance) product is particularly well-suited to DERs. FCR-D has small min bids (0.1MW), flexible bid durations (1 to 6 hours) and pooled measurement. Pools are capped at 3MW, adding a cost (metering) and administrative burden to large portfolios. FFR's 1 second response time limits many DERs, but is otherwise accessible.
In DK1, BSP restrictions on pooling limit the market. Pooling is allowed, but only within the same BRP if mixing generation and demand. BSPs are able to participate in the FCR market, but only BRPs may realistically access the restricted aFRR and mFRR markets.
### Products
| Product | DER suitability | Pooling | Direction | Bidding | Min bid (MW) | Pool size (MW) | Full Activation Time (s) | Bid duration (hrs) | Measurement granularity (s) | Payment |
| ----------- | --------------- | ------- | ------------ | ---------- | ------------ | -------------- | ------------------------ | ------------------ | --------------------------- | --------------------- |
| FCR (DK1) | 3 | Yes | Symmetrical | Daily | 1 | 3 | 30 | 4hrs | 1 | Availability |
| aFRR (DK1) | 1 | Yes | Symmetrical | Monthly | 1 | 10 | 600 | | 1 | Delivery,Availability |
| mFRR (DK1) | 2 | Yes | Asymmetrical | Year,Daily | 10 | 3 | 750 | | 1 | Delivery,Availability |
| FCR-D (DK2) | 4 | Yes | Asymmetrical | Daily | 0.1 | 3 | 30 | 1-6hrs | 1 | Availability |
| FCR-N (DK2) | 2 | Yes | Symmetrical | Daily | 0.1 | 3 | 30 | | 1 | Availability |
| FFR (DK2) | 2 | Yes | Asymmetrical | Daily | 0.3 | 3 | 1 | | 1 | Availability |
# France
Source: https://docs.axle.energy/markets/france
### Overview
France has a mixed balancing market. FCR is procured via the common EU framework, but strict symmetricality requirements limit DER participation. The aFRR market is now active and operational. mFRR and RR are not yet part of the common MARI platform, but do allow some DER access via specified programmes.
France has a country-specific load-shedding product (NEBEF) well-suited to DERs. BSP participation is allowed. Under NEBEF, assets are paid to reduce their consumption compared to their day-ahead forecast.
France also has time-based DSO charges, enabling further cost avoidance if in the BRP role.
The French capacity mechanism (MECAPA - Mécanisme de Capacité) requires electricity suppliers to secure capacity certificates proportional to their customers' consumption during peak periods. This creates an opportunity for DERs to generate additional revenue streams by offering capacity during system stress events.
The Call for Tenders for Demand Response (Appel d'Offres Effacement or AOE) is a specific French mechanism designed to promote demand response. The annual tender allows DERs to bid capacity that can be called upon during peak demand periods. The AOE provides a guaranteed revenue stream through capacity payments, supplementing energy market earnings.
### Products
| Product | DER suitability | Pooling | Direction | Bidding | Min bid (MW) | Full Activation Time (s) | Measurement (s) | Payment |
| ------- | --------------- | ------- | ------------ | ---------- | ------------ | ------------------------ | --------------- | --------------------- |
| FCR | 3 | Yes | Symmetrical | Daily | 1 | 30 | 10 | Availability |
| NEBEF | 4 | Yes | Asymmetrical | Daily | 0.1 | | | Delivery |
| mFRR/RR | 2 | Yes | Asymmetrical | Year,Daily | 10 | 15 | 10 | Delivery,Availability |
| aFRR | 2 | Yes | Asymmetrical | Daily | 1 | 300 | 10 | Availability |
| MECAPA | 4 | Yes | Asymmetrical | Annual | 0.1 | N/A | 30 | Availability |
| AOE | 4 | Yes | Asymmetrical | Annual | 0.1 | 120 | 10 | Availability,Delivery |
# Great Britain
Source: https://docs.axle.energy/markets/gb
### Overview
Wholesale, balancing and flexibility markets in Great Britain are independent from the rest of Europe. There are a large range of programmes that are accessible and lucrative.
The three markets that are best suited to DERs are:
1. The **wholesale** market. This is GB's primary electricity market, and contains deep value for DERs. It has complete geographic coverage of England, Scotland and Wales, and is available to trade all hours of the day. Since December 2024, the GB wholesale market is open to DERs traded by aggregators. Participating assets can be metered at the asset level (if CoP11 approved) or the boundary level (if half-hourly settled).
2. The **Capacity Market**, which offers DERs a lucrative payment for being on standby to turn down in the case of severe grid stress. The CM offers high rewards for minimal intrusion, whilst also using assets to help secure the GB power grid.
3. **Local network flex**, managed by the 6 DNOs in GB. This product is highly local (down to the LV-substation), but highly accessible. Lenient rules on measurement (sub-boundary, or asset), tolerant penalties, and acceptance of non-supplier participation make this a favourite.
Additionally, the TSO (NESO) operates a centrally-dispatched Balancing Mechanism, technically accessible to all assets. However, dispatch today frequently deviates from merit order in favour of large generating units, which limit DER upside. Reforms to BM bid selection are slowly increasing the attractiveness of DER participation, but this is a slow process.
Frequency products are today dominated by the Dynamic suite (DC, DM, DR), which aren't well-suited to most DERs due to a very fast (\1s) response time for the majority of volume. Slower products (e.g., SFFR) are accessible to DERs but are being phased out.
### Products
| Name | DER suitability | Access | Min bid (MW) | Full Activation Time | Payment |
| -------------------------------- | --------------- | ---------- | ------------ | -------------------- | -------------------- |
| Wholesale | 5 | VTP | 0.1 | - | Energy |
| Capacity Market | 4 | Any | 1.0 | 1h | Availability |
| DNO flex | 4 | Any | 0.01 | 15m | Energy,Availability |
| Demand Flexibility Service (DFS) | 4 | Mostly BRP | 1 | 4h | Energy |
| Balancing Mechanism (BM) | 3 | BRP or VLP | 1 | - | Energy |
| Quick Reserve | 3 | Any | 1 | 1m | Energy, Availability |
| Balancing Reserve | 3 | Any | 1 | 10m | Energy, Availability |
| Slow Reserve (future) | 3 | BRP or VLP | 1 | 15m | Energy, Availability |
| FFR (Static) | 3 | Any | 1 | 30s | Availability |
| Network charges | 3 | BRP | 0 | - | Energy |
| Dx (DC, DM, and DR) | 2 | Any | 1 | 1-10s | Availability |
### Upcoming changes
New Reserve products (Quick Reserve and Slow Reserve) are in development, and are becoming more accessible to DERs.
# Germany
Source: https://docs.axle.energy/markets/germany
### Overview
The German balancing market participates in EU cooperation for FCR and aFRR products. The BSP role exists, but only BRPs are effectively able to participate. Pooling is allowed, within the same TSO area (4 TSOs in Germany). Burdensome prequalification and measurement requirements (including DSO approval) limit the scalability of DER participation.
### Products
| Product | DER suitability | Pooling | Direction | Bidding | Min bid (MW) | Full Activation Time (s) | Measurement granularity (s) | Payment |
| ------- | --------------- | ------- | ------------ | ---------- | ------------ | ------------------------ | --------------------------- | --------------------- |
| FCR | 3 | Yes | Symmetrical | Daily | 1 | 30 | 1 | Availability |
| aFRR | 2 | Yes | Asymmetrical | Daily | 1 | 600 | 1 | Delivery,Availability |
| mFRR | 2 | Yes | Asymmetrical | Year,Daily | 1 | 750 | 1 | Delivery,Availability |
# Netherlands
Source: https://docs.axle.energy/markets/netherlands
### Overview
The Dutch market is mostly aligned with the ENTSO-E standard balancing products. The BSP role enables participation in balancing products (minus wholesale and imbalance) for non-suppliers, and lenient pooling rules streamline DER access. Fast response times and symmetrical requirements for frequency products remain a barrier.
Dutch balancing is mostly resolved passively, through imbalance pricing. The Dutch TSO (TenneT) publishes real-time imbalance prices, creating a market mechanism to resolve imbalance without calling upon balancing reserves. Imbalance prices are equivalent to the balancing energy price. DER assets may monetize their flexibility on the imbalance market if controlled via the BRP. When balancing isn't required, the price for balancing products is set at the midpoint of 'Up' and 'Down' bids.
### Products
| Product | DER suitability | Access | Pooling | Direction | Min bid (MW) | Full Activation Time (s) | Measurement granularity (s) | Payment |
| ------- | --------------- | ------ | ------- | ------------ | ------------ | ------------------------ | --------------------------- | ------------------- |
| FCR | 3 | BSP | Yes | Symmetrical | 1 | 30 | 4 | Availability |
| aFRR | 4 | BRP | Yes | Asymmetrical | 1 | 300 | 4 | Energy,Availability |
| mFRR | 3 | BRP | Yes | Asymmetrical | 1 | 300 | 4 | Energy,Availability |
# Sweden
Source: https://docs.axle.energy/markets/sweden
### Overview
Sweden's frequency and balancing products are broadly in-line with the Nordic cooperation model. FCR-D and aFRR are particularly well-suited to DERs due to small minimum bids, asymmetric acceptance, and flexible prequalification. Strict measurement rules require a wallbox connection for EV flexibility. In 2022, these products were typically more lucrative than in neighboring countries.
SVK, Sweden's TSO, allows small (\<0.1MW) assets to be type-qualified and therefore added to a pool without pre-qualification. Although this type qualification is limited to 1MW of additional capacity, the relatively quick (\<8 week) pre-qualification process means this isn't an insurmountable barrier.
The BSP role is not yet active is Sweden, unlike neighboring countries, limiting participation to BRPs.
### Products
| Product | DER suitability | Pooling | Direction | Bidding | Min bid (MW) | Pool size (MW) | Full Activation Time (s) | Bid duration (hrs) | Measurement granularity (s) | Payment |
| ------- | --------------- | ------- | ------------ | ------- | ------------ | -------------- | ------------------------ | ------------------ | --------------------------- | --------------------- |
| FFR | 2 | Yes | Asymmetrical | Daily | 0.3 | 1 | 1 | | 1 | Availability |
| FCR-D | 4 | Yes | Asymmetrical | Daily | 0.1 | 1 | 30 | 1-6hrs | 1 | Availability |
| FCR-N | 2 | Yes | Symmetrical | Daily | 0.1 | 1 | 30 | | 1 | Availability |
| aFRR | 3 | Yes | Symmetrical | Daily | 1 | 1 | 300 | Fixed | 1 | Availability |
| mFRR | 2 | Yes | Asymmetrical | Daily | 5 | N/A | 900 | Fixed | 1 | Availability+Delivery |
# Asset and boundary telemetry
Source: https://docs.axle.energy/tutorial/telemetry
What data we need to participate, and how to send it
We need two kinds of data to participate in a market:
* **operational metering**: readings from the asset to understand and optimize asset usage. We typically need these with as low latency as possible
* **settlement metering**: readings from the asset or site boundary meter, which we send to the market operator as proof of delivery
Operational metering always comes from the asset we're optimising. Settlement metering can also come from the asset (known as **asset-metering**), but in many markets
we're required to obtain readings from the site boundary meter (**boundary-metering**). Axle can obtain boundary meter data for consenting households if you don't already have access to it.
In cases where we use asset metering, we may have to pass additional qualification checks to confirm that the metering provided by the asset is sufficiently accurate.
| Market | Asset Metering Allowed? |
| --------------- | ----------------------- |
| DNO Flex | Yes |
| Capacity Market | Yes (1.5% accuracy) |
| DFS | Yes |
| Wholesale | Yes (CoP11 required) |
[CoP11](../assets/chargers#cop11-qualification) is a UK standard which is required to use an asset's metering in the wholesale market. Axle has extensive
experience qualifying assets for CoP11 - please [get in touch](mailto:hello@axle.energy) to discuss how we can qualify your asset
# Communicating asset telemetry
The kind of telemetry we gather varies by asset.
* Charge Power (kW)
* Plugged in (boolean)
* State of charge (SOC) (%)
* Plugged in (boolean)
* Charge Power (kW)
* State of charge (SOC) (%)
* Inverter Power (kW)
* Grid Power (kW)
* Solar Power (kW)
* Power (kW)
* Temperature (°C)
For sites with a CT clamp installed at the grid boundary, Axle can collect half-hourly CT clamp readings through supported asset integrations, or you can push them to the [Asset Readings](../api-reference/data/readings) endpoint as `boundary_import_energy_kwh` and `boundary_export_energy_kwh`.
CT clamp data measures site-level grid I/O similarly to a boundary meter, and is treated as such when verifying delivery.
## Reading direction and sign convention
When you send readings to the [Asset Readings](../api-reference/data/readings) endpoint, the sign of each value tells us its direction.
For readings that can flow either way, the convention is **positive = import, negative = export**:
* **Import** means power/energy flowing *in* — from the grid into the site, or into the battery (charging).
* **Export** means power/energy flowing *out* — out to the grid, or out of the battery (discharging).
All other reading types are unidirectional and must be non-negative. The table below summarises the `label` values accepted by the endpoint:
| Reading (`label`) | Unit | Sign convention |
| ----------------------------------------- | ---- | ----------------------------------------------------------------------------------- |
| `boundary_import_kw` | kW | Positive = import from the grid, negative = export to the grid |
| `battery_inverter_import_kw` | kW | Positive = into the battery (charging), negative = out of the battery (discharging) |
| `solar_power_kw` | kW | Generation only — always `>= 0` |
| `charger_consumption_energy_kwh` | kWh | Consumption only — always `>= 0` |
| `charger_lifetime_consumption_energy_kwh` | kWh | Cumulative lifetime consumption — always `>= 0` |
| `boundary_lifetime_import_energy_kwh` | kWh | Cumulative lifetime import across the site boundary — always `>= 0` |
| `boundary_lifetime_export_energy_kwh` | kWh | Cumulative lifetime export across the site boundary — always `>= 0` |
| `boundary_import_energy_kwh` | kWh | Half-hourly import across the site boundary — always `>= 0` |
| `boundary_export_energy_kwh` | kWh | Half-hourly export across the site boundary — always `>= 0` |
| `provisional_consumption_kwh` | kWh | Consumption only — always `>= 0` |
| `battery_state_of_charge_pct` | % | Between 0 and 100 (inclusive) |
| `battery_stored_energy_kwh` | kWh | Always `>= 0` |
You can push telemetry to our [Asset Readings](../api-reference/data/readings) endpoint, where you'll find a list of accepted reading typesWe can poll your API or subscribe to webhooks to obtain telemetry.
# Obtaining boundary meter data
Axle can obtain boundary meter data on your behalf, with user's consent and proof of address. [Contact us](mailto:hello@axle.energy) to
discuss the best way to incorporate this consent collection into your product.
# Site and asset lifecycle
Source: https://docs.axle.energy/workflows/asset-lifecycle
How sites and assets move through the platform, and how to manage them as your users change
A user owns a **site** — a physical property — and that site holds the **assets** (EV chargers, batteries and heat pumps) Axle can monitor and control. Enrolment, consents and payments all belong to the user via their site, so the operations below act on a **site**, not on individual users.
Over their lifetime, sites move between different states depending on whether they exist on the platform and whether they're enrolled in a [proposition](/workflows/ev-charging/overview#choosing-a-product) — the agreement that lets Axle participate in a market on the user's behalf.
This page describes those states, the API calls that move between them, and how to handle the everyday changes your users will throw at you: adding an asset, swapping a charger, opting out, or moving house.
## Managing sites
```mermaid theme={null}
%%{init: {'theme': 'base', 'themeVariables': {'edgeLabelBackground': '#ffffff', 'fontSize': '14px'}}}%%
flowchart LR
classDef state fill:#ffffff,stroke:#222,stroke-width:1.5px,color:#222,rx:12,ry:12
A["Unseen / Deleted Inaccessible No active propositions"]:::state
B["Inactive Accessible No active propositions"]:::state
C["Active Accessible Active propositions"]:::state
A -- "/initialise" --> B
B -- "/enrol" --> C
A -- "/onboard" --> C
C -- "/unenrol" --> B
C -- "/offboard" --> A
linkStyle 0,1,2 stroke:#00C1FF,stroke-width:2px
linkStyle 3,4 stroke:#FF3333,stroke-width:2px
```
| State | Exists on platform? | Enrolled in a proposition? | What it means |
| -------------------- | :-----------------: | :------------------------: | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Unseen / Deleted** | No | No | We don't know about this site, or we've removed it. |
| **Inactive** | Yes | No | The site and its assets exist — you can look them up, update them, and add or remove assets — but they're not participating in any proposition. |
| **Active** | Yes | Yes | The site is enrolled and participating. Schedules, dispatches and rewards are all live. |
### Site transitions
Each arrow in the diagram is a single API call.
| Endpoint | From → To | Use it when |
| ------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`onboard`](/workflows/flex-lite/api-reference/onboard) | Unseen → Active | **Preferred** — the common path for opting-in. Creates the site and assets **and** enrols them in one call; reach for it by default. |
| [`initialise`](/workflows/flex-lite/api-reference/initialise) | Unseen → Inactive | You want to create the site and assets, but do work before committing. |
| [`enrol`](/workflows/flex-lite/api-reference/enrol) | Inactive → Active | The commitment point after `initialise` — this begins participation in a proposition (scheduling, readings, rewards). |
| [`unenrol`](/workflows/flex-lite/api-reference/unenrol) | Active → Inactive | **Preferred** — the common path for opting-out. The user is leaving a proposition but staying on the platform. Easily reversible; payments still accessible. |
| [`offboard`](/workflows/flex-lite/api-reference/offboard) | Active → Deleted | The user is leaving entirely. Removes the site and its assets. |
### Opting in
Most integrations sign a user up with a single [`onboard`](/workflows/flex-lite/api-reference/onboard) call, which creates the site and its assets **and** enrols them in one go.
Split it into [`initialise`](/workflows/flex-lite/api-reference/initialise) + [`enrol`](/workflows/flex-lite/api-reference/enrol) only when you need to do work between creating the site and committing it to a proposition — see [Advanced Onboarding](/workflows/flex-lite/advanced-integration) for details.
### Opting out
A user can stop participating in two ways: [`unenrol`](/workflows/flex-lite/api-reference/unenrol) and [`offboard`](/workflows/flex-lite/api-reference/offboard)
In most cases, **unenrol** is the best way for a user to opt-out, and should be the default for a user-initiated opt-out action. It cleanly stops participation — no more readings, scheduling, or rewards — while leaving the site and its assets in place. Because the user remains on the platform, their [payments](/workflows/payments/overview) remain accessible, and you can switch participation back on at any time by re-enrolling the same site.
This is especially relevant when a site is participating in another proposition, like the **Capacity Market**. Unenrol only stops the proposition you name, so unenrolling Flex Lite leaves their Capacity Market participation untouched.
**Offboard** is the heavy option, for when the user is gone for good: they've moved out, or your support team is removing them. It removes the site and its assets entirely, ends every proposition, and cuts off the user's access — including the payments portal.
| | Unenrol | Offboard |
| --------------------------------------------- | ---------- | -------- |
| Participation (scheduling, readings, rewards) | Stopped | Stopped |
| Other propositions (CM, etc.) | Unchanged | Stopped |
| Site & assets | Accessible | Removed |
| Payments | Accessible | Cut off |
Always prompt users to withdraw their outstanding balance before you offboard them — once a site is offboarded, the user can no longer reach the payments portal.
## Managing assets within a site
The lifecycle above acts on the **site**. But a site can gain or lose assets without changing state — a user might add a battery to a property that already has a charger, or replace a charger that's been swapped out.
These are asset-level operations that leave the site enrolled and untouched:
Add an asset to an existing site
Remove a single asset from a site
## Common scenarios
Use [Create asset](/api-reference/entities/asset/post) against the existing `site_id`. The site stays enrolled, and the new asset starts participating once it's eligible. There's no need to re-onboard or re-enrol the site.
Save the returned `asset_id` — you'll need it to send events for that asset.
Use [Delete asset](/api-reference/entities/asset/delete) with the `asset_id`. The rest of the site is unaffected.
If the user is removing their *only* asset and won't be participating any more, consider whether you want to [unenrol](/workflows/flex-lite/api-reference/unenrol) the site instead — or [offboard](/workflows/flex-lite/api-reference/offboard) it if they're leaving for good. See [opting out](#opting-out) for which to reach for.
Treat it as a create followed by a delete: [Create](/api-reference/entities/asset/post) the replacement asset first, then [Delete](/api-reference/entities/asset/delete) the old one. Use a fresh `external_id` for the new asset so your records line up with ours.
The site stays enrolled throughout, so there's no gap in participation for its other assets.
Update the email against the existing `site_id` with the [site `PATCH` endpoint](/api-reference/entities/site/update). Their site, assets, consents and payments all stay intact.
See [Opting out](#opting-out) for the full guidance on choosing between unenrolling and offboarding a site, and on handling any outstanding payment balance.
If you unenrolled the site, it's sitting in the **Inactive** state — the site and assets still exist. Call [enrol](/workflows/flex-lite/api-reference/enrol) with the existing `site_id` to bring it back to **Active**. No need to onboard again.
If you offboarded instead, the site was deleted, so you'll need to [onboard](/workflows/flex-lite/api-reference/onboard) the user again.
A site is tied to a physical property (and its MPAN), so a move is really two separate users at two properties:
1. **Offboard the old site.** The user no longer controls the assets at their previous address, so [offboard](/workflows/flex-lite/api-reference/offboard) it.
Always prompt users to withdraw their outstanding balance before you offboard them — once a site is offboarded, the user can no longer reach the payments portal.
2. **Onboard them at the new property** as a fresh site, if they're taking eligible assets with them or the new home already has them.
The previous occupant should have been [offboarded](/workflows/flex-lite/api-reference/offboard) when they left. The new occupant is a different user, so [onboard](/workflows/flex-lite/api-reference/onboard) them as a new site against the same MPAN. Don't re-use the old site — its consents and payments belong to the previous user, and it stays linked to their account.
## Where next?
Insert pauses into existing charging schedules with a light integration
Hand full charge-plan control to Axle for maximum flexibility revenue
Split onboarding into initialise + enrol
Full request and response specs for every endpoint
# Authentication
Source: https://docs.axle.energy/workflows/axle-vpp/api-reference/auth
POST /auth/token-form
Authenticate using username and password submitted via form data.
This endpoint is compatible with standard OAuth2 clients that use form-based authentication. Returns an access token upon successful authentication.
The returned token is valid for 1 hour, after which a new token must be requested.
# Dispatch webhook
Source: https://docs.axle.energy/workflows/axle-vpp/api-reference/dispatch
The dispatch instruction Axle sends to your webhook during a grid event
During a grid event, Axle sends a dispatch instruction to a webhook URL you provide, as the
`vpp:dispatch:requested` event. One instruction covers every affected asset, setting a target power
for each over a fixed window.
You register your webhook URL when you set up your integration — [get in
touch](mailto:hello@axle.energy) to configure it.
The payload, the envelope it arrives in, the `x-axle-sig` signature, how to respond and the retry
behaviour are all documented on the Webhooks page.
The `vpp:dispatch:requested` payload and the full delivery contract
# Offboard site and assets
Source: https://docs.axle.energy/workflows/axle-vpp/api-reference/offboard
POST /entities/site/{site_id}/offboard-site-and-assets
Offboard a site and all its associated assets in a single operation.
Unenrols from every proposition, and deletes the site and assets in accordance with data retention policies.
Use when the user is leaving entirely — for example, they've moved house, or your support team is removing them. Payments can no longer be accessed afterwards.
If the user is only opting out of one proposition (e.g. toggling Flex Lite off in your app), use `unenrol` instead — it preserves the site and any other propositions they're enrolled in.
# Onboard site and asset
Source: https://docs.axle.energy/workflows/axle-vpp/api-reference/onboard
POST /entities/site/onboard-site-and-asset
**Deprecated** — prefer [`/onboard`](/api-reference/entities/site/onboard) instead.
The new `/onboard` endpoint supports multiple assets per site in a single call and includes improvements in idempotency, resilience and response details.
---
Register a site, asset, and dispatch consent in a single call.
Creates the site and asset, grants consent, and enrols in the relevant flex proposition.
This endpoint will:
1. Create the site
2. Create the asset associated with the site
3. Enrol in the relevant flex proposition
Returns the created site and asset, including their IDs. Fails if the site or asset already exists.
# Send telemetry
Source: https://docs.axle.energy/workflows/axle-vpp/api-reference/telemetry
POST /data/readings
Send asset readings data to Axle using the generic readings format.
This endpoint accepts compressed data using gzip encoding. Returns statuses of the processed readings, indicating whether each reading was accepted or rejected.
**Direction convention:** for readings that can be either positive or negative (e.g. `boundary_import_kw`, `battery_inverter_import_kw`), a positive value denotes *import* (power flowing in from the grid, or into the battery) and a negative value denotes *export* (power flowing out to the grid, or out of the battery). All other reading types are unidirectional; the required sign for each `label` is documented in the `ReadingDataPointLabel` schema below.
# Integration
Source: https://docs.axle.energy/workflows/axle-vpp/integration
Integrate your battery fleet directly with the Axle VPP
This pathway is for battery manufacturers (OEMs) who run their own cloud platform and
want their users to be able to join the Axle VPP.
You send us telemetry, we handle optimisation, market participation and customer relations.
We'll send dispatch instructions to an endpoint you provide for you to execute on your own fleet.
```mermaid theme={null}
sequenceDiagram
participant OEM
participant Axle
Note over OEM,Axle: Setup
OEM->>Axle: Register assets (onboarding)
Note over OEM,Axle: Ongoing
OEM->>Axle: POST /data/readings (telemetry)
Note over OEM,Axle: Grid Event
Axle->>OEM: POST dispatch instruction to your webhook
OEM->>OEM: Apply target power to each asset
```
The integration has three parts:
1. [Onboarding](#1-onboarding)
2. [Telemetry](#2-telemetry)
3. [Dispatch](#3-dispatch)
## 1. Onboarding
Each battery needs a `site_id` and `asset_id` in Axle's system before you can send telemetry or
receive dispatch for it — these are the identifiers used throughout the rest of the integration.
There are two ways to get there.
**What we mean by a "battery".** Axle treats a battery as a complete battery *system* — the
battery, its inverter and its meter. If a site has several battery units behind a single system,
register them as one asset with the combined capacity.
If you already collect address, consent and asset details yourself, call Axle's onboarding API
directly.
Register a site and battery, returns `site_id` and `asset_id`.
Example request
```json theme={null}
{
"site": {
"email": "example@example.com",
"mpan": "1234567890123",
"postcode": "SW1A 1AA",
"street_address": "10 Downing Street",
"gave_boundary_meter_consent_at": "2025-01-01T12:00:00Z"
},
"asset": {
"external_id": "battery-001",
"type": "battery",
"properties": {
"power_kw": 5.0,
"capacity_kwh": 13.5
}
},
"dispatch_consent": {
"dispatch_methods": ["vpp_limited_control"]
}
}
```
Your backend authenticates as described in [Authentication](./api-reference/auth).
For events only mode you should use `vpp_limited_control` in the `dispatch_methods` array.
If you want to allow full control of the battery, use `full_asset_schedule_control` instead.
If you'd rather not build a signup UI, your customers can sign up on a flow Axle hosts and
connect their battery by signing in with their existing account on your platform. Your
platform acts as the OAuth2 provider (authorisation server) and Axle is the client — a
standard authorisation-code flow, the same way we integrate with manufacturers today.
The customer lands on Axle's signup page and begins signup. Axle creates the site at this
point.
The customer picks your brand as their battery manufacturer and clicks "Connect". Axle
redirects them to your OAuth2 authorisation page with `client_id`, `redirect_uri`, `scope`
and `state`.
The customer signs in with their existing account and authorises access — no Axle password
needed. You redirect them back to Axle's callback with an authorisation code, which Axle
exchanges for an access token server-to-server at your token endpoint.
Axle calls your device-list API with the access token to find the customer's devices. The
customer confirms their battery and completes address, MPAN and consent — staying on Axle
throughout, finishing on an Axle-hosted welcome page. Axle creates the asset with your
device serial recorded against it.
Axle POSTs the new asset to an endpoint on your API, authenticated with the same access
token. The payload carries the `asset_id` you'll use for [Telemetry](#2-telemetry) and
[Dispatch](#3-dispatch), together with the device serial so you can match it to your
customer:
```json theme={null}
{
"asset_id": "3c361164-5187-4b4b-8e8a-7b3f67c4d5a8",
"device_serial": "OEM-SERIAL-12345"
}
```
```mermaid theme={null}
sequenceDiagram
participant Customer
participant Axle
participant OEM
Note over Axle,OEM: You are the OAuth2 provider
Customer->>Axle: Start signup on Axle's page
Axle->>Axle: Create site
Customer->>Axle: Select manufacturer, click "Connect"
Axle-->>Customer: Redirect to your OAuth2 sign-in
Customer->>OEM: Sign in & authorise access
OEM-->>Customer: Redirect back to Axle with auth code
Axle->>OEM: Exchange code for access token
Axle->>OEM: Fetch customer's devices (device-list API)
Customer->>Axle: Confirm device, address & consent (stays on Axle)
Axle->>Axle: Create asset
Axle->>OEM: POST asset (asset_id + device serial)
Axle-->>Customer: Show Axle welcome page
```
### What you need to provide
* A standard OAuth2 authorisation-code flow: a hosted sign-in page and a token endpoint
* A `client_id` and `client_secret` issued to Axle
* Axle's redirect URI whitelisted: `https://api.axle.energy/vpp/oauth/{provider}/callback`
* A scope granting access to the customer's devices
* A device-list API returning the customer's devices (serial, power/capacity) for a given
access token
* An endpoint on your API that accepts the asset registration shown above
## 2. Telemetry
Send regular readings for each battery so we can optimise it and verify delivery. We rely mainly
on two readings:
* **Battery state of charge** — `battery_state_of_charge_pct` (0–100)
* **Grid import/export** at the site boundary — `boundary_import_kw`
Grid power is bidirectional, so the sign tells us the direction: **positive = import** from the
grid, **negative = export** to the grid. If you can also send inverter power
(`battery_inverter_import_kw`) and solar generation (`solar_power_kw`), we'll use them to improve
optimisation and determine event earnings.
Push readings to the readings endpoint, which lists every accepted `label` and its sign
convention.
Send **instantaneous readings** (a spot value at a single point in time) at a granularity of
**5–10 minutes**. Because each reading is instantaneous, its start and end timestamps must be the
same.
Push battery and boundary readings to Axle.
If pushing readings doesn't suit your platform, we can also pull readings from custom endpoints
you provide — [get in touch](mailto:hello@axle.energy) to discuss your setup.
## 3. Dispatch
During a grid event we send a dispatch instruction to a webhook URL you provide. Each instruction
covers a single event and sets a target power for every affected asset over a fixed window.
You supply the webhook URL when you set up your integration — [get in
touch](mailto:hello@axle.energy) to register it.
```json theme={null}
{
"event_id": "eab8c867-7c4d-4116-a587-02c9d1f13a16",
"issued_timestamp": "2026-06-29T17:02:37.448917+00:00",
"start_time": "2026-06-30T20:30:00+00:00",
"end_time": "2026-06-30T21:30:00+00:00",
"assets": [
{ "asset_id": "3c361164-5187-4b4b-8e8a-7b3f67c4d5a8", "power_kw": -6.915 },
{ "asset_id": "1a161164-5187-4b4b-8e8a-7b3f67c4d5a8", "power_kw": -4.915 }
]
}
```
`power_kw` uses the same sign convention as telemetry: **positive = charge** (import into the
battery), **negative = discharge** (export). Apply the target power to each asset for the window
between `start_time` and `end_time`.
The target power applies at the **inverter**. We normally dispatch at the inverter's maximum power
rating, and we account for the fact that household load can limit the power the battery actually
delivers.
We give you at least **30 minutes' notice** before an event (normally 4 hours to 1 day ahead)
via the dispatch webhook. We do not currently run very-short-notice (sub-30-minute) events.
Full schema for the dispatch instruction we send to your webhook.
If you already have your own dispatch API, we can work with custom dispatch endpoints instead of
the webhook above — [get in touch](mailto:hello@axle.energy) to discuss your setup.
### Important points
* Customers should be able to override dispatch instructions at any time.
* The battery should return to its original mode of operation after the dispatch event has
finished
# Overview
Source: https://docs.axle.energy/workflows/axle-vpp/overview
Optimise behind-the-meter batteries against tariffs and flexibility markets
## What is the Axle VPP?
Axle's Virtual Power Plant (VPP) product provides the backend infrastructure to optimise behind-the-meter batteries
against tariffs and flexibility markets. We handle battery dispatch, optimisation and market participation.
## How does the VPP work?
Register your battery assets with us and we'll dispatch commands to optimise the battery in one of two modes:
We send daily schedules optimising the battery's operation to maximise revenue and grid impact.
We only send commands during periods when the grid is under stress and the battery can provide value.
### Events
Grid events are times when the grid is under stress and the VPP takes action.
These typically last between 30-60 minutes and we expect roughly 3-5 events per month.
These are handled in both modes, but for optimised mode we will add these into the daily schedules.
We can optionally notify the customer over email the day before an event, based on your preferences.
### Daily Schedules
These are only sent daily in optimised dispatch mode and are designed to maximise the battery's revenue and grid impact.
These are based on:
* User's electricity tariff
* Solar production forecast
* Electricity consumption forecast
* Battery cycling and other battery health considerations
* Flexibility market prices
## Getting started
You send us telemetry and execute our dispatch instructions on your own fleet. Best if you're a
battery manufacturer running your own cloud platform.
# Collecting and validating addresses
Source: https://docs.axle.energy/workflows/collecting-addresses
Market participation starts with an address we can associate to a meter
To participate in a market, we need to know the [MPAN](https://smartmetercheck.citizensadvice.org.uk/meters/new?fuel_type=electricity) of the user's meter. This is the primary identifier that actors within the electricity system use to identify a consumer of electricity.
This page describes how to collect addresses in a way that allows us to retrieve the MPANs associated with them, and
fall back to asking users for their MPAN directly if necessary.
If you've already collected addresses and would like to retrieve MPANs from them, see [Retrieving meter details](./find-mpan).
# Recommended workflow
1. Collect the user's postcode
2. Use that to retrieve a list of valid addresses for that postcode
3. Present the user with that list of addresses to choose from
4. If the user's address is not in the list, ask them to enter the MPAN directly
# Implementing address collection
1. Use the [search meters](../api-reference/meter/search) endpoint to search by `postcode`. This returns a list of possible matches.
2. If the user selects an address from the list, you can use the `ref` from that address to retrieve the meter details using the [meter info by ref](../api-reference/meter/get-by-ref) endpoint.
3. If the user does not find their address in the list, you can ask them to enter their MPAN directly. You can validate this MPAN using the [validate MPAN](../api-reference/validation/mpan) endpoint. Once you've collected a valid MPAN, you can retrieve the meter details using the [meter info by mpan](../api-reference/meter/get-by-mpan) endpoint.
You can use our address collection component to streamline collection of valid addresses. This will implement the logic
[described above](#recommended-workflow) and provide a user-friendly interface for your users to select their address.
For more details see the docs for the [Address Collection Component](../components/components/address).
# Migrating to Smart Charging
Source: https://docs.axle.energy/workflows/ev-charging/advanced/migrating-to-smart-charging
Upgrade from Flex Lite to Smart Charging for full schedule control and higher revenue
If you've already integrated Flex Lite and want to unlock higher revenue and full schedule optimisation, this guide covers exactly what changes when upgrading to Smart Charging.
## Why upgrade?
| | Flex Lite | Smart Charging |
| ----------------------- | ----------------------------------- | ----------------------------------------------- |
| **Scheduling model** | Inserts pauses into *your* schedule | Axle generates the *entire* schedule |
| **Optimisation window** | Single 30-min pause | Full charging session |
| **Re-scheduling** | One-shot (at plug-in) | Continuous — Axle re-optimises as prices change |
| **Revenue potential** | Lower | Higher — more flexible capacity to sell |
Smart Charging maximises flexibility revenue because Axle controls the full charging window, not just a single pause.
## What changes
* **Tariff data** — send the user's energy tariff so Axle can cost-optimise charging
* **Intent** — send the user's energy requirement (kWh) and ready-by time, which sets the minimum requirements for the schedule Axle will build
* **Separate plug-in / plug-out events** — replace the combined `plug-in-schedule` call so Axle knows the charging window
* **Schedule endpoint** — Axle calls your API with an OCPP schedule payload whenever a new schedule is generated
* **Half-hourly readings** — report consumption so Axle can verify delivery and calculate rewards
* **`plug-in-schedule` endpoint** — replaced by separate plug-in event + async schedule delivery
* **Schedule in API response** — schedules arrive via a call to your API with an OCPP schedule payload, not in the plug-in response
* **Authentication** — same OAuth token flow
* **Site & asset model** — same entities, same IDs
* **Onboarding** — same `onboard-site-and-asset` (or advanced `initialise` / `enrol`) flow, though Smart Charging uses the `full_asset_schedule_control` proposition instead of `limited_pause`
* **Offboarding** — same `offboard-site-and-assets` endpoint
* **Payments** — same rewards and transaction endpoints
## Integration steps
These are the changes you need to make on top of your existing Flex Lite integration. For full detail on each step, see the [Smart Charging integration guide](/workflows/smart-charging/integration).
### 1. Send tariff data
Axle needs the user's energy tariff to optimise charging cost. Send it once at enrolment time, then update whenever the tariff changes.
Send or update a user's tariff
### 2. Collect and send intent
Collect the user's charging requirement and deadline:
* **kWh** — how much energy to add
* **Ready-by time** — when the vehicle needs to be ready
Send intent once at enrolment time, then update whenever it changes.
Send or update a user's charging intent
### 3. Replace plug-in-schedule with separate events
Instead of a single `plug-in-schedule` call that returns a schedule synchronously, send separate plug-in and plug-out events. Schedules are delivered asynchronously in OCPP format via an API call to your endpoint.
Notify Axle when a vehicle plugs in
Notify Axle when a vehicle unplugs
You should also send a plug event when a user is first enrolled to inform Axle of the initial plug state.
### 4. Set up schedule endpoint
Axle sends charging schedules to your platform in OCPP format via an API call. You need to expose an endpoint that can receive and apply these schedules to the charger.
Axle will send a schedule:
* When the user plugs in
* When the user is already plugged in and one of the following events happens:
* The user's tariff is updated
* The user's intent changes
* Axle reoptimises in response to changes in the wider market
See the format of schedule pushes from Axle
### 5. Report half-hourly readings
Send consumption readings so Axle can verify delivery and calculate rewards.
Report asset consumption readings
### 6. Enrol users in Smart Charging
Once the above integration work is complete, you can start enabling users. Enrol them in the `full_asset_schedule_control` proposition via the [enrol endpoint](/workflows/flex-lite/api-reference/enrol):
```json theme={null}
{
"proposition": "full_asset_schedule_control"
}
```
Sites can be enrolled in multiple propositions simultaneously, so this won't affect the existing Flex Lite enrolment. Once you've verified Smart Charging is working, unenrol from `limited_pause` via the [unenrol endpoint](/workflows/flex-lite/api-reference/unenrol) to keep things clean and avoid ambiguity.
## Sequence diagram
```mermaid theme={null}
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Charger
participant Your API
participant Axle API
Note over Your API,Axle API: One-time setup per user
Your API->>Axle API: POST /entities/site/{site_id}/enrol {proposition: "full_asset_schedule_control"}
Axle API-->>Your API: {status: "enrolled"}
Your API->>Axle API: POST /entities/asset/{asset_id}/event/tariff
Axle API-->>Your API: 200 OK
Note over Charger,Axle API: Each charging session
Charger->>Your API: User plugs in
Your API->>Axle API: POST /entities/asset/{asset_id}/event/plug-in
Axle API-->>Your API: OCPP SetChargingProfile
Your API->>Charger: Apply schedule
Note over Charger,Axle API: Schedule may be re-sent if tariff or intent changes, or Axle reoptimises
Charger->>Your API: User unplugs
Your API->>Axle API: POST /entities/asset/{asset_id}/event/plug-out
Note over Your API,Axle API: Reporting
Your API->>Axle API: POST /data/readings (half-hourly consumption)
Note over Your API,Axle API: User offboarding
Your API->>Axle API: POST /entities/site/{site_id}/unenrol
Axle API-->>Your API: {"site_id": "...", "status": "unenrolled"}
```
## Migration checklist
Use this to track your migration progress:
Send each user's tariff to Axle.
Add UI to collect kWh requirement and ready-by time, and send via the intent endpoint.
Switch from `plug-in-schedule` to separate `plug-in` and `plug-out` event calls.
Expose an endpoint to receive and apply OCPP charging profiles from Axle.
Report consumption data via the readings endpoint.
Run a full charging session through the sandbox to verify the integration.
Enrol each user in `full_asset_schedule_control`. Once verified, unenrol from `limited_pause`.
## Next steps
Full integration walkthrough
Complete API documentation
Example charging profile payload
# Eligibility & Consent
Source: https://docs.axle.energy/workflows/ev-charging/eligibility-and-consent
Determine which users can participate in EV charging flexibility and get them signed up
Before onboarding users, you need to collect their address, check eligibility, and obtain consent. The recommended approach is to collect this information at the point when a user first connects an asset in the app.
User creates app account
User connects charger to app account
User inputs address and MPAN
Check user eligibility for flex
eligibleineligible
User provides informed consent
User finishes set up and enters proposition
Legacy onboarding flow
Click on relevant steps in the flowchart to jump to the corresponding point in the video. Note that the eligibility check happens in the background.
## Address collection
An MPAN (Meter Point Administration Number) is required for eligibility checks and participation in a proposition. We use the address to get a user's MPAN.
You may already be collecting the user's address but our purpose built address lookup flow is optimised for matching to an MPAN.
Learn how to implement an address collection workflow
Learn how to retrieve meter details
## Eligibility
Use the check-eligibility endpoint to find out which propositions a site qualifies for, to determine whether to proceed with the user proposition. Pass the MPAN, and optionally a charger model.
Check the eligibility of a site without persisting any data.
You control eligibility - by default, we will only let market-qualified users participate in flex propositions, but we can expand access based on your preferences.
## Consent
Once you've identified an eligible user, you need to obtain their consent to participate.
This is typically done through an update to your Terms & Conditions that users must agree to before onboarding. You'll need to capture a **timestamp** indicating when the user agreed to these terms.
Store the consent timestamp - you'll need to provide it during the onboarding process to confirm the user has agreed to participate.
## Next Steps
Once you've collected:
* **MPAN** - from the address lookup
* **Consent timestamp** - when the user agreed to the terms
You're ready to integrate with your chosen product:
Start onboarding eligible users to Smart Charging
Start onboarding eligible users to Flex Lite
# Overview
Source: https://docs.axle.energy/workflows/ev-charging/overview
Find the right EV charging flexibility solution for you
Axle offers two EV charging solutions which enable you and your users to earn money from grid flexibility. Both solutions work by adjusting charging schedules to support the grid.
## Controlling via car or charger
Axle can control EV charging through either a **smart charger** or the **vehicle itself**.
| | **Via charger** | **Via car** |
| -------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| **Connection** | Direct to charger API or OCPP | Via car manufacturer API |
| **Geofencing** | Not required — charger is always at home | Required — Axle only controls charging when the car is at home (boolean signal, no location data needed) |
| **Best for** | Charger OEMs, white-label apps | Car OEMs, white-label apps |
When controlling via car, Axle uses a **"car is home" signal from the car manufacturer** before issuing any charging instructions — no location or GPS data is required, just a boolean indicating the vehicle is at home. In practice, this commonly means the manufacturer API only forwards plug-in events to Axle when the car is at home. This ensures Axle never issues charging instructions while the vehicle is at a public or workplace charger.
## Choosing a product
**A note on terminology:** Throughout these docs, we use **product** to refer to the integration choice you make as a client (Flex Lite or Smart Charging), and **proposition** to refer to what your end users sign up for — a proposition combines the product with configuration such as reward amounts.
| | **[Smart Charging](/workflows/smart-charging/overview)** | **[Flex Lite](/workflows/flex-lite/overview)** |
| ---------------------- | ---------------------------------------------------------------- | --------------------------------------------------------- |
| **How it works** | Axle creates full charge plans optimised for flexibility revenue | Axle inserts pauses into your existing charging schedules |
| **Integration effort** | Medium — requires intent collection and OCPP schedule handling | Low — just 3 API endpoints |
| **UX changes** | Charging UX built around Axle schedules | Minimal — simply capture customer consent |
| **Revenue potential** | Higher — full schedule optimisation | Moderate — limited pause-based flexibility |
| **Best for** | Platforms building charging UX around Axle | Platforms with existing scheduling which want to add flex |
## Next steps
Both products share the same starting point to assess eligibility and collect consent:
Gather the information needed to check eligibility and then get consent
# Overview
Source: https://docs.axle.energy/workflows/ev-charging/paying-users/earning
How users earn and manage their flexibility rewards
During proposition configuration, we will agree with you the timing and magnitude of user rewards. We'll then handle
delivering those rewards to users.
## Paying users
Axle provides a complete payments solution to help users manage and withdraw their earnings. The system is built on Stripe and supports UK bank accounts.
### Balance and withdrawal URL
We generate a URL where your users can:
* View their rewards balance
* Link a UK bank account
* Withdraw earned rewards to their bank
Learn about the payment system architecture and flow
Detailed guide for generating payment URLs and handling redirects
### Optional: Transaction history
If you want to display transaction history directly in your application (rather than sending users to the payment portal), you can retrieve this data programmatically
via the [transaction history](/workflows/ev-charging/paying-users/transactions) endpoint.
# Get balance withdrawal URL
Source: https://docs.axle.energy/workflows/ev-charging/paying-users/payments
GET /rewards/{site_id}/url/balance
Get a temporary link to an Axle-hosted form through which users can view and withdraw rewards. Requires an organisational bearer token.
# Get transactions
Source: https://docs.axle.energy/workflows/ev-charging/paying-users/transactions
GET /rewards/{site_id}/transactions
Get history of transactions for the site. Requires an organisational bearer token.
# Retrieving meter details
Source: https://docs.axle.energy/workflows/find-mpan
Identifying user MPANs and their eligibility
To participate in a market, we need to know the [MPAN](https://smartmetercheck.citizensadvice.org.uk/meters/new?fuel_type=electricity) of the user's meter. This is the primary identifier that actors within the electricity system use to identify a consumer of electricity. We can typically retrieve this from
the user's address, and we provide tooling for you to do this.
If you are looking to collect better address data, see [Collecting and
validating addresses](collecting-addresses).
We'll help you retrieve the meter details for an address, including the eligible markets for that meter.
Sites with an export meter will have two MPANs: one for import and one for
export. We index sites by the import MPAN, and you can use the import MPAN
to retrieve the meter details.
# Retrieving the MPAN
## Quick lookup
Use the [MPAN lookup endpoint](../api-reference/meter/get-by-address) to retrieve the MPAN for a given address. If we can find a single unambiguous MPAN, we'll return it.
If we cannot unambiguously identify the meter at the provided address, this
endpoint will return `202` or `422`. In this case, you should use the
exhaustive lookup.
## Exhaustive lookup
If you'd like finer-grained control over the lookup, you can use a multistep process to retrieve partially matching
addresses, identify the best match, and then retrieve the relevant meter details.
Use the [search meters](../api-reference/meter/search) endpoint to search by `postcode`.
This returns a list of possible address matches.
Let the user select their address from the list, for example via a dropdown or scrollable UI element.
Pass the `ref` from their chosen address to the [meter info by ref](../api-reference/meter/get-by-ref) endpoint
to retrieve the meter details.
# Understanding market eligibility
Different sites are eligible for different flexibility markets.
When you retrieve the meter details for a site, we return supplemental data about that meter. These include the
markets that the site is eligible for.
You can also retrieve these for known MPANs using the [meter info](../api-reference/meter/get-by-mpan) endpoint.
# Advanced Onboarding
Source: https://docs.axle.energy/workflows/flex-lite/advanced-integration
Decompose onboarding into initialise + enrol when you need to do work between the two steps
`onboard` covers the standard flow in a single call. If you need to do work between site creation and enrolment, you can call the constituent endpoints — `initialise` and `enrol` — separately.
## How it works
The flow splits into two API calls:
1. **Initialise** — creates/upserts the site and assets, and returns eligibility information. No enrolment side effects.
2. **Enrol** — the commitment point. Allows scheduling and begins market participation.
```mermaid theme={null}
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Client
participant Axle API
Note over Client,Axle API: Step 1: Initialise
Client->>Axle API: POST /entities/site/initialise
Axle API-->>Client: site_id, asset_ids, eligibility
Note over Client: Review eligibility internally
Note over Client,Axle API: Step 2: Enrol
Client->>Axle API: POST /entities/site/{site_id}/enrol
Axle API-->>Client: {status: "enrolled"}
alt Flex Lite
Client->>Axle API: POST /entities/asset/{asset_id}/event/plug-in-schedule
Axle API-->>Client: Returns modified charging schedule
else Smart Charging
Client->>Axle API: POST /entities/asset/{asset_id}/event/intent
Client->>Axle API: POST /entities/asset/{asset_id}/event/plug-in
Axle API-->>Client: OCPP SetChargingProfile (async)
end
```
Reach for this flow if you need to do something between site creation and enrolment. Otherwise, most use cases should be covered by `onboard`.
To reverse enrolment, use the [`unenrol`](/workflows/flex-lite/api-reference/unenrol) endpoint — this stops the specified proposition without deleting the site, leaving other propositions and the option for the *same* user to re-enrol later intact. To remove the site entirely, use [`offboard`](/workflows/flex-lite/api-reference/offboard) instead.
Create/upsert site and assets
Enrol site in a proposition
Unenrol from a proposition
# Authentication
Source: https://docs.axle.energy/workflows/flex-lite/api-reference/auth
POST /auth/token-form
Authenticate using username and password submitted via form data.
This endpoint is compatible with standard OAuth2 clients that use form-based authentication. Returns an access token upon successful authentication.
The returned token is valid for 1 hour, after which a new token must be requested.
# Enrol site
Source: https://docs.axle.energy/workflows/flex-lite/api-reference/enrol
POST /entities/site/{site_id}/enrol
Enrol a site in a proposition.
This is the commitment point — it activates the proposition for the site.
Idempotent: if the site is already enrolled, returns `already_enrolled` without making changes.
If the site previously withdrew from this proposition, returns 422. Set `override_withdrawal` to true to re-enrol.
# Initialise site and assets
Source: https://docs.axle.energy/workflows/flex-lite/api-reference/initialise
POST /entities/site/initialise
Initialise a site with one or more assets.
This endpoint upserts the site and assets (creating them if they don't exist, or filling in missing fields if they do), and returns eligibility information.
**No enrollment side effects**: Use the `/enrol` endpoint to commit to enrollment.
# Offboard site and assets
Source: https://docs.axle.energy/workflows/flex-lite/api-reference/offboard
POST /entities/site/{site_id}/offboard-site-and-assets
Offboard a site and all its associated assets in a single operation.
Unenrols from every proposition, and deletes the site and assets in accordance with data retention policies.
Use when the user is leaving entirely — for example, they've moved house, or your support team is removing them. Payments can no longer be accessed afterwards.
If the user is only opting out of one proposition (e.g. toggling Flex Lite off in your app), use `unenrol` instead — it preserves the site and any other propositions they're enrolled in.
# Onboard
Source: https://docs.axle.energy/workflows/flex-lite/api-reference/onboard
POST /entities/site/onboard
Initialise a site and its assets, and enrol them in a proposition in a single call.
This endpoint will:
1. Upsert the site and asset(s)
2. Enrol in the relevant flex proposition
The response returns the site and asset IDs you'll use in subsequent calls, and the enrolment outcome.
Onboarding is idempotent and atomic — sending the same site or asset returns the existing record with any new fields merged in; if enrolment fails, nothing is stored.
# Send plug-in event with schedule
Source: https://docs.axle.energy/workflows/flex-lite/api-reference/plug-in-schedule
POST /entities/asset/{asset_id}/event/plug-in-schedule
Submit a charging schedule and receive a modified schedule.
# Unenrol site
Source: https://docs.axle.energy/workflows/flex-lite/api-reference/unenrol
POST /entities/site/{site_id}/unenrol
Unenrol a site from a proposition.
Withdraws the site from the proposition and prevents further scheduling. Does not delete the site or its assets.
Use when the user is opting out of this proposition specifically — for example, they toggled it off in your app. The site and assets are kept, any other propositions they're enrolled in (such as Capacity Market) are unaffected, and the user can re-enrol later and still access their payments.
If the user is leaving entirely (moving house, support-led removal, etc.), use `offboard-site-and-assets` instead.
Idempotent: if the site is already unenrolled, returns `already_unenrolled` without making changes.
# Integration
Source: https://docs.axle.energy/workflows/flex-lite/integration
Integrate Flex Lite scheduling into your EV charging platform
This guide walks you through integrating Flex Lite into your EV charging platform. By the end, you'll be able to send user charging schedules and receive optimized schedules that enable grid flexibility while maintaining user control.
### Before you begin...
Prefer to explore the API hands-on? Grab a focused OpenAPI spec or a ready-to-run Postman collection covering every endpoint in this flow, from eligibility through onboarding to rewards.
OpenAPI 3.1 spec for the Flex Lite endpoints — import into your own tooling or hand to an LLM.
Pre-wired collection with auth set up — run **Authenticate** and the token is saved for every other request.
```mermaid theme={null}
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant User
participant Client
participant Axle API
rect rgb(235, 235, 235)
Note over User,Axle API: Eligibility
User->>Client: Enter postcode
Client->>Axle API: GET /meter/search?postcode=...
Axle API-->>Client: List of matching addresses
Client->>User: Present address list
User->>Client: Select their address
Client->>Axle API: GET /meter/by-ref/{meter_ref}
Axle API-->>Client: MPAN + meter details
Client->>Axle API: POST /entities/site/check-eligibility {mpan, asset_model}
Axle API-->>Client: {eligible, propositions}
Client->>User: Present offer, request consent
User->>Client: Accept terms (capture consent timestamp)
Note over Client: Ready to onboard with MPAN + consent timestamp
end
rect rgb(224, 247, 250)
Note over User,Axle API: Onboarding
Note over Client: Eligible site with MPAN + consent timestamp
Client->>Axle API: POST /entities/site/onboard {proposition: "limited_pause"}
Axle API-->>Client: site_id, asset_ids, enrolment status
Client-->>User: Enrolled
end
rect rgb(255, 248, 225)
Note over User,Axle API: Charging
User->>Client: Plugs in EV
Client->>Axle API: POST /entities/asset/{asset_id}/event/plug-in-schedule
Axle API-->>Client: Modified charging schedule (with pauses)
Note over Client: Apply modified schedule to charger
end
rect rgb(235, 235, 235)
Note over User,Axle API: Viewing rewards
User->>Client: Tap "View rewards"
Client->>Axle API: GET /rewards/{site_id}/url/balance
Axle API-->>Client: Temporary URL
Client-->>User: Open Axle-hosted rewards form
end
rect rgb(252, 228, 236)
Note over User,Axle API: Offboarding
alt User opts out of Flex Lite (e.g. leaves the flex program in your app)
User->>Client: Disable Flex Lite
Client->>Axle API: POST /entities/site/{site_id}/unenrol
Axle API-->>Client: Site unenrolled
else User leaves entirely (e.g. moves house, removed by support team)
User->>Client: Remove account / change address
Client->>Axle API: POST /entities/site/{site_id}/offboard-site-and-assets
Axle API-->>Client: Site and assets removed
end
end
```
## Prerequisites
* An Axle API token (see [Authentication](./api-reference/auth))
* Basic site information (MPAN, address, postcode)
* Ability to detect plug-in events
* User's desired charging schedule
* Confirmed [eligibility & consent](/workflows/ev-charging/eligibility-and-consent) and user consent
## Step 1: Onboard your site and assets
Register the site and EV charger with Axle and enrol them in the `limited_pause` proposition — the proposition that lets Axle insert pauses into the user's charging schedule.
Create or update a site and its assets, and enrol them in a proposition.
If you want to enable [payments](/workflows/payments/overview) for your users, include `site.email` in the onboard payload. A user email is required to process payments through our provider, Stripe. We also send payment-failure notifications and, if enabled, two-factor authentication codes to this address.
### Example
```json Request theme={null}
{
"site": {
"mpan": "1234567890123",
"postcode": "SW1A 1AA",
"street_address": "10 Downing Street",
"email": "user@example.com",
"gave_boundary_meter_consent_at": "2026-04-28T12:00:00Z"
},
"assets": [
{
"external_id": "charger-001",
"type": "charger",
"asset_model": "axle_charger_v1",
"installation_date": "2026-03-26",
"properties": {
"power_kw": 7.4
}
}
],
"proposition": "limited_pause"
}
```
```json Response theme={null}
{
"site": {
"site_id": "a1b2c3d4-...",
"site_created": true,
"fields_updated": [],
"warnings": []
},
"assets": [
{
"asset_id": "e5f6g7h8-...",
"external_id": "charger-001",
"asset_created": true,
"fields_updated": [],
"warnings": []
}
],
"enrolment": {
"site_id": "a1b2c3d4-...",
"status": "enrolled"
}
}
```
Save `site.site_id` and each `assets[].asset_id` from the response — you'll need the asset ID for sending schedules and the site ID for rewards and offboarding.
You can also [look up IDs later](/api-reference/entities/asset/get-by-external-id) using each asset's `external_id`.
## Step 2: Send plug-in event with charging schedule
When a user plugs in their EV, send their desired charging schedule to the plug-in-schedule endpoint. Axle will return a modified schedule optimized for grid flexibility.
Send a charging schedule and receive an optimised version
### Example
```json Request theme={null}
{
"charging_schedule": [
{
"start_timestamp": "2025-06-27T22:00:00+00:00",
"end_timestamp": "2025-06-28T06:00:00+00:00"
}
]
}
```
```json Response theme={null}
{
"modified_charging_schedule": [
{
"start_timestamp": "2025-06-27T22:00:00+00:00",
"end_timestamp": "2025-06-28T01:00:00+00:00"
},
{
"start_timestamp": "2025-06-28T02:00:00+00:00",
"end_timestamp": "2025-06-28T06:00:00+00:00"
}
]
}
```
Notice the response includes a 1-hour pause (01:00–02:00) — this is where the grid flexibility happens. The user earns rewards for this pause while still receiving the charging they need.
## Common questions
User actions always take precedence. If a user modifies their schedule or boosts charging after receiving the modified schedule from Axle, apply their requested changes and send Axle a new schedule.
Not every plug-in event will result in a modified schedule. Axle only adjusts
schedules when there's a grid flexibility opportunity that provides value. If
no modification is beneficial, you'll receive the original schedule back
unchanged.
Axle's optimization ensures the user receives sufficient energy based on their needs. The typical modification is a 30 minute pause, which maintains adequate charging for most use cases. If the original schedule is already tight, we may not modify it.
## Step 3: Stop participation (when needed)
There are two ways to stop a user's participation. Pick based on what's actually happening:
* **Unenrol** when the user is opting out of Flex Lite — for example, they leave the flex proposition in your app. The site and assets stay on the platform, any other propositions they're enrolled in (such as Capacity Market) are unaffected, and they can re-enrol later and still access their payments.
* **Offboard** when the user is leaving entirely — for example, they've moved house, or your support team is removing them. This unenrols from every proposition and deletes the site and its assets per data-retention policies. Payments can no longer be accessed afterwards, so prompt the user to withdraw any outstanding balance first.
If in doubt, use **unenrol**. For the full picture, see the [Site and asset lifecycle](/workflows/asset-lifecycle#opting-out).
Stop this proposition without deleting the site
Remove the site and its assets
## Sending readings (CoP11 assets)
Flex Lite doesn't strictly require asset readings to monetise flexibility. For settlement we use **boundary meter readings** from the site's MPAN as proof of delivery, which Axle can obtain on consenting users' behalf — capture this consent with `site.gave_boundary_meter_consent_at` when you onboard (see [Step 1](#step-1-onboard-your-site-and-assets)).
If your asset is [CoP11-qualified](/assets/chargers#cop11-qualification), you should additionally send asset readings via our API. CoP11 lets Axle use **asset metering** in the wholesale market, which can unlock more value and broaden eligibility.
Push half-hourly asset readings — see the accepted reading types
Not sure whether your asset is CoP11-qualified? Axle has extensive experience qualifying assets for CoP11 — [get in touch](mailto:hello@axle.energy) to discuss. See [Asset and boundary telemetry](/tutorial/telemetry) for the full picture on what data we need and how to send it.
## Next Steps
At this point, your users are onboarded and their schedules are being optimised. Now it's time to set up revenue and payments.
Learn how to submit readings and manage user payments
## Further Reading
Need more control? Use initialise and enrol for a step-by-step onboarding flow.
Ready for more revenue? See what changes when upgrading to Smart Charging.
# Overview
Source: https://docs.axle.energy/workflows/flex-lite/overview
Monetize flexibility while maintaining user control over charging schedules
## What is Flex Lite?
Flex Lite gives users the ability to opt into "giving an hour to the grid". Axle will adjust their charging schedules, and in return, users will receive cash rewards.
This approach balances flexibility with user control, allowing participants to earn rewards and help the grid while maintaining oversight of their charging behaviour.
## How does Flex Lite work?
1. When a user plugs in their EV, you communicate the user's preset charging schedule.
2. We then adjust this schedule so that it includes a pause which will allow the user to receive cash rewards from the grid.
3. We return the modified schedule to you, which you apply to the charger or vehicle.
### What is the UX for Flex Lite?
Flex Lite requires **minimal frontend changes**. A simple toggle can allow users to opt in or out of Flex Lite. No changes to the charging schedule UX are required.
### Scheduling behavior
The default behavior for Flex Lite is to insert a **single 30 minute pause** into the user's existing charging schedule.
This pause is placed at the optimal time to maximize revenue while ensuring the user still meets their charging needs.
The pause duration can be adjusted based on user preferences or grid requirements. Longer pauses may yield higher rewards but could impact user satisfaction.
## Why use Flex Lite?
* **You want to minimize integration**: Flex Lite can be implemented with just 3 endpoints
* **Minimal app changes**: You only need to collect consent from users, no charging UX changes
* **Customers are already happy with scheduling**: No need to change the paradigm
Flex Lite gives Axle limited control over charging. This minimises integration and UX complexity. If you'd
like to maximise revenue and grid impact, consider fully managed [Smart Charging](/workflows/smart-charging/overview).
## Getting started
Ready to integrate? Start by checking eligibility and then follow our integration guide:
Determine which users can participate
Complete walkthrough of onboarding users and sending schedules
Reward users for participation
For detailed API specifications, see the [API Reference](/workflows/flex-lite/api-reference/auth) section.
# Integration Guide
Source: https://docs.axle.energy/workflows/payments/integration
How to integrate the Axle payments portal into your application
## Prerequisites
To generate payment portal URLs, you'll need your organisational API credentials. See [Authentication Guide](/api-reference/sandbox#use-your-credentials-to-obtain-a-token).
Use an organisational bearer token when calling the rewards endpoints.
### User email
A user email is required to process payments through our provider, Stripe. We also send payment-failure notifications and, if enabled, two-factor authentication codes to this address.
The right place to supply the email depends on how you're integrated with us — check the integration guide for the product you're using (Smart Charging, Flex Lite, VPP, etc.) for the field to set when you create or onboard the user.
## Generating payment portal URLs
Use the [Get Balance Withdrawal URL](/api-reference/rewards/withdrawal-url) endpoint to generate a URL for users to access their payments portal.
**Response**:
```json theme={null}
{
"balance_withdrawal_url": "https://payments.axle.energy/auth/ext/{jwt_token}",
"url_expiry_timestamp": "2024-01-15T14:30:00Z"
}
```
## Two-factor authentication
When `enforce_two_factor_auth` is enabled, users must verify their email address before they can link a bank account via Stripe. Balance and transaction-history views are not gated. We send a verification code to the user's registered email and they must enter it to proceed.
**Currently supported verification methods**: Email only — see the prerequisites above for how to supply it.
## Redirect handling
The `redirect_uri` parameter specifies where users are sent after completing their action in the payments portal.
Implementation depends on how users open the balance page URL:
* If opening in a browser external to your app, `redirect_uri` should be a deep link back to your app
* If it's in a modal browser pop-up within your app, it should be a 'magic string' which allows them to exit the modal
We don't currently append any additional information (such as success/failure status or transaction IDs) to the redirect URI. If there's specific information that would be useful for your application flow, please let us know and we can look at adding it to our implementation.
## When to hide transactions
The `hide_transactions` parameter is useful in two scenarios:
1. **One-off payment flow**: When you want a focused experience where users simply withdraw and return, without browsing their full history
2. **Avoiding duplication**: When you already display transaction history in your own app and don't want users seeing the same information twice
## Auto-redirect behaviour
When `auto_redirect_to_setup` is `true`:
* If the user has no bank account linked, they skip the balance page entirely and go straight into the bank account setup flow
* If the user already has a bank account, they see the normal balance page
This is useful for streamlined onboarding flows where you want users to set up their payment method without distraction.
## Example: Python
```python theme={null}
import requests
API_BASE = "https://api.axle.energy"
API_TOKEN = "your_bearer_token"
def get_payment_url(site_id: str) -> str:
response = requests.get(
f"{API_BASE}/rewards/{site_id}/url/balance",
params={
"enforce_two_factor_auth": True,
"redirect_uri": "/rewards/balance",
"hide_transactions": False,
"auto_redirect_to_setup": False,
},
headers={"Authorization": f"Bearer {API_TOKEN}"}
)
response.raise_for_status()
return response.json()["balance_withdrawal_url"]
```
## JWT token structure
The generated URL contains a JWT with the following claims:
```json theme={null}
{
"sub": "organisation_name",
"internal_site_id": "uuid",
"allowed_origin": "*.axle.energy",
"type": "rewards",
"permitted_url_stubs": ["/rewards"],
"two_factor_auth": true,
"redirect_uri": "/rewards/balance",
"hide_transactions": false,
"auto_redirect_to_setup": false,
"exp": 1705329000
}
```
**Default expiry**: 60 minutes from generation.
## Session lifetime and expiry
The portal validates the JWT on every navigation event. The token is never refreshed server-side during a session — the one exception is two-factor verification, which re-issues a token with a fresh 60-minute window.
If the token expires mid-session, the user is silently redirected to `redirect_uri` on their next navigation.
A URL is valid for the full 60 minutes and can be reused within that window — URLs are not single-use, and there is no persistent session beyond the token. The recommended pattern is to generate a fresh URL on each page entry, so users never start on an already-expiring token.
## Recommended integration pattern
For the best user experience, we recommend following this pattern:
1. Call the [Get Balance Info](/api-reference/rewards/balance-info) endpoint, which returns:
* The user's current balance
* The minimum withdrawal threshold
* Other relevant account information
2. **Use that info to control UI state**:
* Show/enable a "withdraw" button if balance is above the minimum threshold
* Grey out or hide the button if below the threshold
* Display helpful messaging about why withdrawal isn't available
3. **Call [Get Withdrawal URL](/api-reference/rewards/withdrawal-url)** when the user clicks the enabled button
This pattern prevents users from entering the withdrawal flow only to discover they can't actually withdraw, creating a smoother experience.
# Overview
Source: https://docs.axle.energy/workflows/payments/overview
Pay users for their flexible energy usage with Axle's payments portal
We use [Stripe](https://stripe.com) as our payment provider, leveraging their [Global Payouts](https://docs.stripe.com/global-payouts) functionality.
We provide you with a page where your users can:
1. View their rewards balance
2. Link a UK bank account
3. Withdraw earned rewards to their bank
The URL for this page is generated via our API and secured with a JWT token.
**Availability**: UK only (GB bank accounts, GBP currency)
## Requirements
A user email is required to process payments through our provider, Stripe. We also send payment-failure notifications and, if enabled, two-factor authentication codes to this address.
The right place to supply the email depends on how you're integrated with us — check the integration guide for the product you're using (Smart Charging, Flex Lite, VPP, etc.) for the field to set when you create or onboard the user.
## High-level flow
```mermaid theme={null}
sequenceDiagram
participant App as Partner App
participant API as api.axle.energy
participant Portal as payments.axle.energy
participant Stripe
App->>API: 1. Generate JWT URL
API-->>App: Return secure URL
App->>Portal: 2. Redirect user
Portal->>Portal: 3. User views balance
Portal->>Stripe: 4. Create & verify bank account
Stripe-->>Portal: Confirmation of Payee result
Portal->>Portal: 5. User requests withdrawal
Portal->>Stripe: 6. Create outbound payment
Stripe-->>API: 7. Webhook: payment posted
Portal-->>App: 8. Redirect back to app
```
## Environments
| Environment | Payments Portal | API | Use Case |
| ----------- | ----------------------------- | ------------------------- | ----------------------- |
| Sandbox | `payment-sandbox.axle.energy` | `api-sandbox.axle.energy` | Development and testing |
| Production | `payments.axle.energy` | `api.axle.energy` | Live users |
Use the Sandbox environment for all development and testing. When you're ready to go live, switch to Production.
## Security considerations
* JWT tokens expire after 60 minutes (payment failure emails use 48-hour tokens)
* Generating URLs requires an organisational bearer token
* Two-factor authentication can be enforced via the `enforce_two_factor_auth` parameter (email verification only) — it gates the bank-account linking flow, not balance or transaction-history views
* Bank account details are validated via Confirmation of Payee (CoP) before any withdrawal is processed
* A URL is valid for the full 60 minutes and can be reused within that window; there is no persistent session beyond the token. We recommend generating a fresh URL on each page entry to avoid presenting an already-expiring token to the user
## Support
For integration support, contact: [support@axle.energy](mailto:support@axle.energy)
# User Flow
Source: https://docs.axle.energy/workflows/payments/user-flow
Deep dive into the payments portal user experience
# Workflow
Users can:
1. View their current balance and transaction history
2. Link a UK bank account for withdrawals
3. Withdraw their rewards balance
## View current balance and transaction history
Transactions can be hidden via the `hide_transactions` parameter when generating the payments portal URL - see [Integration](./integration#when-to-hide-transactions).
## Bank account linking
If 2FA is enabled, users must first complete two-factor authentication before linking a bank account
User enters their bank details (sort code, account number)
Axle performs Confirmation of Payee (CoP) verification
User reviews the verification result
User must click "Confirm" to complete setup
### Confirmation of Payee (CoP)
CoP is a UK banking verification service that checks the provided name against the bank's records.
**Match States**:
| State | Description | User Action |
| ------------- | ---------------------------------------------------- | ---------------------------- |
| Full Match | Name matches bank records exactly | Proceed with confidence |
| Partial Match | Name partially matches (e.g., nickname vs full name) | User can confirm if correct |
| Mismatch | Name does not match bank records | User should verify details |
| Unavailable | Bank doesn't support CoP verification | User can proceed at own risk |
## Withdrawals
### How withdrawals work
1. User clicks "Withdraw" in the payments portal
2. Axle initiates a bank transfer to the linked account
3. Funds typically arrive within 1-2 business days
4. Transaction appears in user's history as a withdrawal
### Withdrawal restrictions
* **Minimum threshold**: Typically £10 (configurable per organisation)
* **Cooldown period**: Some organisations require a waiting period before the first withdrawal (configurable)
### Payment failures
Payments can fail or be returned for various reasons—incorrect account details, closed accounts, or bank-specific restrictions. See [Stripe's documentation on payout failure codes](https://docs.stripe.com/api/payouts/object#payout_object-failure_code) for a full breakdown of failure reasons.
When a payment fails or is returned:
1. The withdrawal transaction is reversed (balance restored)
2. The bank account is archived (user must re-link)
3. User receives an email with a link to update their bank details
4. The retry link expires after 48 hours
# White-Labelling
Source: https://docs.axle.energy/workflows/payments/white-labelling
Customise the payments portal with your own branding
The payments portal can be customised to match your brand, including custom URLs and email senders. These customisations will take additional development time on both sides.
## Custom payment URLs
It's possible to host the payments portal on a subdomain of your own domain (e.g., `payments.yourdomain.com` instead of `payments.axle.energy`).
This requires collaboration with whoever manages your DNS records to configure domains on your side.
## Custom email sender
By default, emails (such as 2FA codes and payment failure notifications) are sent from our generic Axle Energy email address.
This requires collaboration with whoever manages your DNS records to configure domains on your side.
## Custom email templates
We can customise the email templates with your branding on request. Relevant emails include:
* Two-factor authentication verification codes
* Payment failure notifications
* Withdrawal confirmation emails
This requires collaboration with your design team to customise templates.
## Payment portal theming
The portal UI can be styled to match your brand colours and include your logo.
This requires collaboration with your design team to customise the UI.
## Getting started
Contact [support@axle.energy](mailto:support@axle.energy) to discuss white-labelling options for your organisation.
# Authentication
Source: https://docs.axle.energy/workflows/price-curves/api-reference/auth
POST /auth/token-form
Authenticate using username and password submitted via form data.
This endpoint is compatible with standard OAuth2 clients that use form-based authentication. Returns an access token upon successful authentication.
The returned token is valid for 1 hour, after which a new token must be requested.
# Offboard site and assets
Source: https://docs.axle.energy/workflows/price-curves/api-reference/offboard
POST /entities/site/{site_id}/offboard-site-and-assets
Offboard a site and all its associated assets in a single operation.
Unenrols from every proposition, and deletes the site and assets in accordance with data retention policies.
Use when the user is leaving entirely — for example, they've moved house, or your support team is removing them. Payments can no longer be accessed afterwards.
If the user is only opting out of one proposition (e.g. toggling Flex Lite off in your app), use `unenrol` instead — it preserves the site and any other propositions they're enrolled in.
# Onboard site and asset
Source: https://docs.axle.energy/workflows/price-curves/api-reference/onboard
POST /entities/site/onboard-site-and-asset
**Deprecated** — prefer [`/onboard`](/api-reference/entities/site/onboard) instead.
The new `/onboard` endpoint supports multiple assets per site in a single call and includes improvements in idempotency, resilience and response details.
---
Register a site, asset, and dispatch consent in a single call.
Creates the site and asset, grants consent, and enrols in the relevant flex proposition.
This endpoint will:
1. Create the site
2. Create the asset associated with the site
3. Enrol in the relevant flex proposition
Returns the created site and asset, including their IDs. Fails if the site or asset already exists.
# Get site price curve
Source: https://docs.axle.energy/workflows/price-curves/api-reference/price-curve
GET /entities/site/{site_id}/price-curve
Half-hourly data of prices per MWh of flexed energy for the asset. Prices are returned from the current Settlement Period until 23:00 UK time on the same day or next day. Next day prices are available from 14:00 UK time.
# Send asset readings
Source: https://docs.axle.energy/workflows/price-curves/api-reference/readings
POST /data/readings
Send asset readings data to Axle using the generic readings format.
This endpoint accepts compressed data using gzip encoding. Returns statuses of the processed readings, indicating whether each reading was accepted or rejected.
**Direction convention:** for readings that can be either positive or negative (e.g. `boundary_import_kw`, `battery_inverter_import_kw`), a positive value denotes *import* (power flowing in from the grid, or into the battery) and a negative value denotes *export* (power flowing out to the grid, or out of the battery). All other reading types are unidirectional; the required sign for each `label` is documented in the `ReadingDataPointLabel` schema below.
# Overview
Source: https://docs.axle.energy/workflows/price-curves/overview
Integrate flex into your existing asset optimisation
## What is price curve integration?
Price curve integration allows you to optimize your assets using localized flex prices provided by Axle, while maintaining full control over your optimization logic and constraints.
This allows you to retain your existing control systems, while unlocking new revenue streams by responding to market signals.
## How it works
For each site, Axle provides a half-hourly price curve that combines different market incentives we're exposed to. These prices reflect:
* **Wholesale energy prices**: Day-ahead and intraday market prices
* **Ancillary services**: Demand response and reserve products
* **Local flexibility requirements**: DNO flex opportunities localized to that site
You integrate these prices into your existing optimization logic, allowing you to make cost-optimal decisions while respecting all your local constraints.
### How will Axle participate in markets with my assets?
When we use a price curve, we're not directly controlling your asset. Instead, we bid into markets based on the flexibility we expect to be available, given the price-responsive behavior of your asset. Since we don't know exactly how your asset will respond to price signals, we typically participate in a fairly conservative way which protects us from non-delivery risk.
For settlement, we participate [via the site's boundary meter](/tutorial/telemetry#obtaining-boundary-meter-data) where available. In addition, every Price Curve integration is required to send half-hourly asset consumption readings via [`POST /data/readings`](/workflows/price-curves/api-reference/readings) — see [Asset and boundary metering](/tutorial/telemetry) for the accepted reading types per asset. We use these readings to verify delivery against the price curve and reconcile revenue back to your asset.
### How should I interpret the price curve?
The price curve represents the **gross marginal cost of electricity consumption** at the site for each half-hour period.
Breaking that down:
* **Gross**: these values do not reflect the revenue share between you and Axle. It is your responsibility
to incorporate the contractual specifics in your use of the price curve. For instance, if you're optimizing for user
cost, and users are receiving 50% of gross value, then you probably want to multiply the price curve by 0.50
* **Marginal Cost**: these are not absolute values. They represent the value of consuming more or less electricity relative
to your existing plan. If your default consumption is 6 kWh and the price curve gives you a value of 20p/kWh, then
we're stating that you will earn 20p if you consume 5kWh, and lose 20p if you consume 7kWh.
The price curve is a good guide as to the relative cost at different times, and we expect most clients to use it this way.
For instance, consider a situation where the price curve peaks at 12p/kWh at 17:00-18:00, but is only 8p/kWh at 16:00-17:00. If you expect your asset to consume energy at 17:00, shifting 1 kWh of expected usage at 17:00 to 16:00 will earn an additional 4p.
#### How do we know your "existing plan"?
Payment in flexibility markets is relative to your baseline - what would have happened had you not flexed
the asset.
Defining the baseline is a complicated topic in demand-side flexibility. It's implemented in different ways in different markets,
and is often changed by market facilitators.
We abstract away the baseline in our price curve calculation. This won't always be 100% accurate - if the baseline
differs from your true plan from the asset, then our estimate of the marginal cost might be slightly wrong.
The price curve is indicative, but non-binding. We are not guaranteeing earnings. The best way to build confidence in
the values provided in the price curve and how they materialize as revenue is to run a pilot with real assets and observe
the result.
## When should I use price curves?
Price curve integration is ideal when:
* **You have existing optimization**: You're already running control algorithms for your assets
* **Local constraints matter**: You have constraints that are difficult to surface to external systems (e.g., minimum hot water tank temperature, user preferences, battery state of charge)
* **You want control**: You prefer to maintain direct control over asset operations
* **Privacy is important**: Local optimisation variables (state of charge, user preferences, tank temperatures, scheduling constraints) stay in your system. You only share the half-hourly consumption readings we need for settlement
With price curve integration, Axle doesn't receive direct feedback on whether you've dispatched the asset as instructed. This means we bid more conservatively in markets, as our ability to compensate for non-delivery is lower. For maximum revenue potential, consider event-based or delegated control approaches.
## Comparison with other dispatch methods
Short-lived flexibility events in response to grid signals. Suitable for infrequent, temporary interruptions.
Axle takes full responsibility for asset operation over extended periods. Ideal when you want to fully outsource optimization to Axle.
## Supported asset types
Price curve integration works with:
* **Heating**: incorporate into existing thermal optimization
* **EV Chargers**: incorporate into an existing smart charging algorithm
* **Batteries**: optimize for arbitrage within an existing optimization
## Getting started
Ready to integrate? Our [tutorial](/workflows/price-curves/tutorial) walks you through:
1. Onboarding a site with an asset
2. Retrieving your first price curve
3. Sending asset readings
4. Offboarding sites and assets
For detailed API specifications, see the [API Reference](/api-reference/entities/site/price-curve) section.
# Tutorial
Source: https://docs.axle.energy/workflows/price-curves/tutorial
Onboard a site and pull your first price curve
This tutorial walks you through onboarding your first site with a flexible asset (EV charger, battery, or heat pump) and retrieving a price curve. By the end, you'll have a complete setup ready for price-based optimization.
```mermaid theme={null}
sequenceDiagram
participant Client
participant Axle API
Note over Client,Axle API: Step 1: Onboard
Client->>Axle API: POST /entities/site/onboard-site-and-asset
Axle API-->>Client: Returns site_id & asset_id
Note over Client,Axle API: Step 2: Retrieve price curves
loop Ongoing optimization
Client->>Axle API: GET /entities/site/{site_id}/price-curve
Axle API-->>Client: Returns half-hourly prices
Note over Client: Optimize asset using price signals
end
Note over Client,Axle API: Step 3: Send asset readings
loop Each settlement period
Client->>Axle API: POST /data/readings
Axle API-->>Client: 200 OK
end
Note over Client,Axle API: Step 4: Offboard (when needed)
alt Consumer withdraws consent or moves house
Client->>Axle API: POST /entities/site/{site_id}/offboard-site-and-assets
Axle API-->>Client: Site and assets removed
end
```
## Prerequisites
* An Axle API token (see [Authentication](./api-reference/auth))
* A valid MPAN (electricity meter number) - use the [Find MPAN workflow](/workflows/find-mpan) if needed
* Permission for Axle to [retrieve boundary meter data](/tutorial/telemetry#obtaining-boundary-meter-data) for the site on your behalf
* Basic site information (address, postcode)
* The ability to send half-hourly asset consumption readings (see [Step 3](#step-3-send-asset-readings))
## Step 1: Onboard your site and assets
The onboarding endpoint creates a site, asset, and dispatch consent in a single atomic operation. This ensures your setup is complete and ready for price-responsive operations.
View the OpenAPI spec with interactive request examples and response schemas.
### Example requests by asset type
Note that we do not require an explicit dispatch consent for price-curve based operation, so the `dispatch_methods` array is empty.
```json theme={null}
{
"site": {
"mpan": "1234567890123",
"postcode": "SW1A 1AA",
"street_address": "10 Downing Street",
"gave_boundary_meter_consent_at": "2025-01-01T12:00:00Z"
},
"asset": {
"external_id": "charger-001",
"type": "charger",
"properties": {
"power_kw": 7.4
}
},
"dispatch_consent": {
"dispatch_methods": []
}
}
```
Note that we do not require an explicit dispatch consent for price-curve based operation, so the `dispatch_methods` array is empty.
```json theme={null}
{
"site": {
"mpan": "1234567890123",
"postcode": "SW1A 1AA",
"street_address": "10 Downing Street",
"gave_boundary_meter_consent_at": "2025-01-01T12:00:00Z"
},
"asset": {
"external_id": "battery-001",
"type": "battery",
"properties": {
"power_kw": 5.0,
"capacity_kwh": 13.5
}
},
"dispatch_consent": {
"dispatch_methods": []
}
}
```
Note that we do not require an explicit dispatch consent for price-curve based operation, so the `dispatch_methods` array is empty.
```json theme={null}
{
"site": {
"mpan": "1234567890123",
"postcode": "SW1A 1AA",
"street_address": "10 Downing Street",
"gave_boundary_meter_consent_at": "2025-01-01T12:00:00Z"
},
"asset": {
"external_id": "heatpump-001",
"type": "heat_pump",
"properties": {
"power_kw": 3.5,
"cop": 3.2
}
},
"dispatch_consent": {
"dispatch_methods": []
}
}
```
The API returns `site.id` and `asset.id` values - **save the `site.id`** for the next step.
For more granular control of sites and assets, see the full [Sites API Reference](/api-reference/entities/site/get).
## Step 2: Retrieve the price curve
Once you're site is onboarded, we will register it in the market. Once registered, you can start pulling half-hourly price curves to inform your optimization.
We'll only populate the price curve once the asset is registered in the market; we will not expose prices until the
asset is able to earn revenue. This typically takes **5 working days**.
View the full API documentation with request examples and response schemas.
**Price curve timing**
* **Current day prices**: Available from the current settlement period until 23:00 UK time
* **Next day prices**: Available from 14:00 UK time
* **Update frequency**: Check for new prices after 14:00 for next-day optimization
## Step 3: Send asset readings
Asset readings are required for every Price Curve integration. Axle uses your half-hourly consumption readings to verify that flexibility was delivered, calculate settlement against the price curve, and reconcile revenue back to your asset.
Post half-hourly consumption readings for each onboarded asset.
The accepted reading types vary by asset (charge power, state of charge, inverter power, temperature, etc.). See [Asset and boundary metering](/tutorial/telemetry) for the full list per asset type and the alternative ways to deliver readings (push to our API, or have us poll yours).
Readings must be submitted by the end of SD+1 (the day after the settlement date). A common approach is to send all readings for a given settlement date at 16:00 the following day. Late or missing readings reduce the revenue we can claim against the price curve for those periods.
## Step 4: Offboard sites and assets
When a consumer withdraws consent or disconnects the asset, you should offboard the site to cleanly remove it and all associated assets from the Axle platform.
View the API documentation for offboarding sites and their associated assets.
The offboarding endpoint removes the site and all associated assets in a single operation, ensuring:
* All dispatch consents are revoked
* Market positions are closed out appropriately
* Asset and site data is archived according to retention policies
**More granular control of assets** is available via the [Assets API](/api-reference/entities/asset).
We offer a comprehensive API for asset management, including adding/removing individual assets and updating dispatch consents.
# Authentication
Source: https://docs.axle.energy/workflows/smart-charging/api-reference/auth
POST /auth/token-form
Authenticate using username and password submitted via form data.
This endpoint is compatible with standard OAuth2 clients that use form-based authentication. Returns an access token upon successful authentication.
The returned token is valid for 1 hour, after which a new token must be requested.
# Send intent
Source: https://docs.axle.energy/workflows/smart-charging/api-reference/intent
POST /entities/asset/{asset_id}/event/intent
Send notification of an intent being updated on an asset.
# Offboard site and assets
Source: https://docs.axle.energy/workflows/smart-charging/api-reference/offboard
POST /entities/site/{site_id}/offboard-site-and-assets
Offboard a site and all its associated assets in a single operation.
Unenrols from every proposition, and deletes the site and assets in accordance with data retention policies.
Use when the user is leaving entirely — for example, they've moved house, or your support team is removing them. Payments can no longer be accessed afterwards.
If the user is only opting out of one proposition (e.g. toggling Flex Lite off in your app), use `unenrol` instead — it preserves the site and any other propositions they're enrolled in.
# Onboard
Source: https://docs.axle.energy/workflows/smart-charging/api-reference/onboard
POST /entities/site/onboard
Initialise a site and its assets, and enrol them in a proposition in a single call.
This endpoint will:
1. Upsert the site and asset(s)
2. Enrol in the relevant flex proposition
The response returns the site and asset IDs you'll use in subsequent calls, and the enrolment outcome.
Onboarding is idempotent and atomic — sending the same site or asset returns the existing record with any new fields merged in; if enrolment fails, nothing is stored.
## Supplying `site.email`
A user email is required to process payments through our provider, Stripe. We also send payment-failure notifications and, if enabled, two-factor authentication codes to this address. See the [Payments overview](/workflows/payments/overview) for details.
# Plug-in event
Source: https://docs.axle.energy/workflows/smart-charging/api-reference/plug-in
POST /entities/asset/{asset_id}/event/plug-in
Send notification of a car being plugged in.
Tariff, vehicle and intent information may be optionally provided.
If a tariff is included, it must comply with the OSM opening hours standard (https://wiki.openstreetmap.org/wiki/Key:opening_hours) for off-peak times and have a valid timezone, or a 400 error will be returned.
# Plug-out event
Source: https://docs.axle.energy/workflows/smart-charging/api-reference/plug-out
POST /entities/asset/{asset_id}/event/plug-out
Send notification of a car being unplugged from an asset.
# Send readings
Source: https://docs.axle.energy/workflows/smart-charging/api-reference/readings
POST /data/readings
Send asset readings data to Axle using the generic readings format.
This endpoint accepts compressed data using gzip encoding. Returns statuses of the processed readings, indicating whether each reading was accepted or rejected.
**Direction convention:** for readings that can be either positive or negative (e.g. `boundary_import_kw`, `battery_inverter_import_kw`), a positive value denotes *import* (power flowing in from the grid, or into the battery) and a negative value denotes *export* (power flowing out to the grid, or out of the battery). All other reading types are unidirectional; the required sign for each `label` is documented in the `ReadingDataPointLabel` schema below.
# Send tariff
Source: https://docs.axle.energy/workflows/smart-charging/api-reference/tariff
POST /entities/asset/{asset_id}/event/tariff-update
Send notification of a tariff update for an asset.
The `off_peak_times` field must comply with the OSM opening hours standard (https://wiki.openstreetmap.org/wiki/Key:opening_hours) and have a valid timezone, or a 400 error will be returned.
The same off-peak times apply every day, day-of-week selectors are not supported. Use `,` to separate multiple time ranges. Common examples:
- Same-day off-peak (e.g. midnight to 7am): `00:00-07:00`
- Overnight off-peak (e.g. 11:30pm to 5:30am): `00:00-05:30, 23:30-24:00`
# Unenrol site
Source: https://docs.axle.energy/workflows/smart-charging/api-reference/unenrol
POST /entities/site/{site_id}/unenrol
Unenrol a site from a proposition.
Withdraws the site from the proposition and prevents further scheduling. Does not delete the site or its assets.
Use when the user is opting out of this proposition specifically — for example, they toggled it off in your app. The site and assets are kept, any other propositions they're enrolled in (such as Capacity Market) are unaffected, and the user can re-enrol later and still access their payments.
If the user is leaving entirely (moving house, support-led removal, etc.), use `offboard-site-and-assets` instead.
Idempotent: if the site is already unenrolled, returns `already_unenrolled` without making changes.
# Integration
Source: https://docs.axle.energy/workflows/smart-charging/integration
Integrate Smart Charging into your platform
This guide walks you through the integration of Smart Charging into your EV charging platform.
Smart Charging works whether the asset is a **charger** or a **car**. If you're connecting via the vehicle, see [Cars](/assets/cars) for notes on the geofencing behaviour that determines when Axle controls charging.
There are three core workflows to support:
1. **Onboarding & offboarding**: registering the site & asset with Axle, and assigning responsibility for charging
2. **Charging**: sending information about the user's requirements, plug-in and plug-out events, and receiving charging schedules
3. **Reporting**: sending charging session data
```mermaid theme={null}
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Charger
participant Your API
participant Axle API
Note over Your API,Axle API: One-time setup per user
Your API->>Axle API: POST /entities/site/onboard {proposition: "full_asset_schedule_control"}
Axle API-->>Your API: {site_id, asset_ids, enrolment: {status: "enrolled"}}
Your API->>Axle API: POST /entities/asset/{asset_id}/event/tariff
Axle API-->>Your API: 200 OK
Note over Charger,Axle API: Each charging session
Charger->>Your API: User plugs in
Your API->>Axle API: POST /entities/asset/{asset_id}/event/plug-in
Axle API-->>Your API: OCPP SetChargingProfile
Your API->>Charger: Apply schedule
Note over Charger,Axle API: Schedule may be re-sent if tariff or intent changes, or Axle reoptimises
Charger->>Your API: User unplugs
Your API->>Axle API: POST /entities/asset/{asset_id}/event/plug-out
Note over Your API,Axle API: Reporting
Your API->>Axle API: POST /data/readings (half-hourly consumption)
Note over Your API,Axle API: User offboarding
Your API->>Axle API: POST /entities/site/{site_id}/unenrol
Axle API-->>Your API: {"site_id": "...", "status": "unenrolled"}
```
# 1. Onboarding & offboarding
## Onboard your site and asset
Register the site and EV charger with Axle and enrol them in the `full_asset_schedule_control` proposition — the proposition that lets Axle issue full charging schedules to the asset.
Create or update a site and its assets, and enrol them in a proposition.
If you plan to enable [payments](/workflows/payments/overview) for your users, include `site.email` in the onboard payload. A user email is required to process payments through our provider, Stripe. We also send payment-failure notifications and, if enabled, two-factor authentication codes to this address.
Save `site.site_id` and each `assets[].asset_id` from the response — you'll need the asset ID to send tariff, intent, and plug-in/plug-out events, and the site ID for rewards and offboarding.
## Stop participation
There are two ways to stop a user's participation. Pick based on what's actually happening:
* **Unenrol** when the user is opting out of Smart Charging specifically — for example, they turn it off in your app or withdraw consent. The site and assets stay on the platform, any other propositions they're enrolled in (such as Capacity Market) are unaffected, and they can re-enrol later and still access their payments.
* **Offboard** when the user is leaving entirely — for example, they've moved house, or your support team is removing them. This unenrols from every proposition and deletes the site and its assets per data-retention policies. Payments can no longer be accessed afterwards, so prompt the user to withdraw any outstanding balance first.
If in doubt, use **unenrol**. For the full picture, see the [Site and asset lifecycle](/workflows/asset-lifecycle#opting-out).
Stop this proposition without deleting the site
Remove the site and its assets
***
# 2. Charging
To deliver Smart Charging, we need to know the customer's tariff, intent and plugin/plugout events. We also need you to be able to receive and implement updated charging schedules.
We will issue charging schedules when a user plugs in; when a user updates their tariff or intent; or in response to wider market changes.
## Tariff
We need to know the user's tariff in order to optimise charging. You should send an update whenever the tariff changes.
Send or update a user's tariff
## Collect and communicate intent
We need to know how much energy is required and when the vehicle needs to be ready. Together, we refer to this as the user's **intent**.
### Energy requirements
We need to know how much energy is required in kWh. There are different ways to arrive
at this number.
| User provides | Notes |
| ---------------- | ----------------------------------------------------- |
| **kWh to add** | User describes requirement in kWh |
| **Hours to add** | Requires approximation of charge power |
| ***Nothing*** | Can be approximated or fixed to e.g. 4 hours charging |
### Ready-by time
We need to know the deadline to deliver the energy requirements. You can collect ready-by with a standard time picker.
For more customisation, you can collect separate ready-by times for different:
* days of the week
* vehicles
And communicate these via API when the relevant vehicle plugs in.
### Sending intent
Our recommendation is to communicate intent whenever it changes, to ensure Axle scheduling always uses the most recent values.
Send or update a user's charging intent
Changes to intent while the car is plugged in can be issued to the same endpoint.
## Plugin and plugout events
Notify Axle when the vehicle plugs in and unplugs.
**Controlling via car?** Axle relies on a **"car is home" signal from the car manufacturer** before acting on plug-in events — no location or GPS data is needed, only a boolean indicating the vehicle is at home. In practice, this often means your API only forwards plug-in events to Axle when the car is at home; Axle treats every plug-in it receives as a home charge.
Notify Axle when a vehicle plugs in.
Notify Axle when a vehicle unplugs.
## Receiving charging schedules
Axle will issue new schedules:
* When the user first plugs in
* When the user is already plugged in and one of the following events happens:
* The user's tariff is updated
* The user's intent changes
* Axle reoptimises in response to changes in the wider market
In all cases, these schedules are issued `async`.
To issue schedules, we need you to make an endpoint available to which we can send API requests.
If you already have an existing API that you think will support smart charging,
we can discuss integrating with that directly. However, our recommendation is
to integrate with our OCPP style request format.
We've developed an OCPP-based format to support smart charging, which provides the most robust, flexible solution. You can see an example of the dispatch events we send [here](/api-reference/examples/get-example-ocpp-charging-profile).
The benefits of building according to OCPP include:
* Specification of charge energy in an industry-standard format
* Stacking of schedules allows fallbacks in case devices go offline. For instance, our schedules should be overlaid
upon a "default" schedule which always charges consumers during off-peak times
Worked examples showing how different intents and tariffs affect the schedules Axle sends
## Special case: Communicating parameters upon enrolment
At the time of enrolment, we expect to immediately receive the information we need to schedule a charge. This means you should:
* Call either plug-in or plug-out to communicate the plug state of the charger at the moment of enrolment
* Set the user's tariff
* Set the intent — this can be a default value or the user's first input
Once we have these 3 pieces of information, we can schedule a charge.
***
# 3. Reporting
## Send readings
Send half-hourly consumption readings so Axle can verify delivery and calculate rewards.
Notify Axle of asset consumption readings
***
## Next Steps
Learn how to manage user payments and withdrawals
```
```
# Overview
Source: https://docs.axle.energy/workflows/smart-charging/overview
Monetize flexibility through managed charging
## What is Smart Charging?
Smart Charging is a fully managed charging solution. Axle will design precise charge plans which meet user charging requirements while supporting the grid, maximising overall flexibility revenue. This revenue can be shared with participants as cash rewards.
## How does Smart Charging work?
Rather than charging a vehicle immediately upon plugin, Smart Charging allocates charging to the time of lowest cost. To do this, we need to know:
* How much energy is required (kWh)
* When the vehicle needs to be ready by (time of day)
We'll take these requirements, combine them with the customer's tariff, and optimize them with
respect to the flexibility markets to formulate an exact charging schedule.
## Why use Smart Charging?
* **You want to maximise flexibility revenue**: Smart Charging provides full control over charging schedules ensuring precise optimisation
* **You're happy with the new customer experience**: Your current scheduling process may need to be changed
* **You're comfortable with more technical work**: The integration has moderate complexity to it
Smart Charging maximises control and therefore flexibility revenue. However if you'd like to retain your current scheduling and minimise integration complexity, consider [Flex Lite](/workflows/flex-lite).
## Getting started
Ready to integrate? Start by checking eligibility and then follow our integration guide:
Determine which users can participate
Complete walkthrough of onboarding users and configuring Smart Charging
Worked examples showing how different intents and tariffs affect the schedules Axle sends
Reward users for participation
For detailed API specifications, see the [API Reference](/workflows/smart-charging/api-reference/auth) section.
# Charging scenarios
Source: https://docs.axle.energy/workflows/smart-charging/scenarios
Worked examples showing how intents and tariffs affect the schedules Axle sends
This page walks through concrete examples of the charging schedules Axle sends for different intents and tariffs. Use it to reason about what your users will experience, and how to handle the edge cases.
## Scenario 1: Overnight charge
The user plugs in after work and wants the car ready by morning. This is the default happy path.
| [Intent](/workflows/smart-charging/api-reference/intent) | Value |
| -------------------------------------------------------- | ------------------------------------ |
| Energy required | 30 kWh |
| May charge at peak | `false` *(default)* |
| Charging mode | `maximise_charging_time` *(default)* |
Axle charges throughout the off-peak window, comfortably meeting the 30 kWh intent. The charger idles during peak rates before and after.
Axle sets `limit` to `99999.0` as an "unlimited" signal. The charger draws at its rated power during each charging period. See the [integration guide](/workflows/smart-charging/integration#receiving-charging-schedules) for how to receive these payloads.
```json theme={null}
{
"chargingProfileId": null,
"transactionId": null,
"stackLevel": 1100,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": null,
"validFrom": null,
"validTo": null,
"chargingSchedule": {
"duration": 32400,
"startSchedule": "2026-04-14T22:00:00+00:00",
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 0.0, "numberPhases": 1 },
{ "startPeriod": 5400, "limit": 99999.0, "numberPhases": 1 },
{ "startPeriod": 27000, "limit": 0.0, "numberPhases": 1 }
],
"minChargingRate": 0.0
}
}
```
***
## Scenario 2: Minimising energy delivered
`charging_mode` controls whether Axle fills the full off-peak window or stops once the stated intent is met.
| [Intent](/workflows/smart-charging/api-reference/intent) | Value |
| -------------------------------------------------------- | ------------------- |
| Energy required | 10 kWh |
| May charge at peak | `false` *(default)* |
Under the default `charging_mode: maximise_charging_time`, Axle treats the energy intent as a minimum, not a cap, and charges through all remaining off-peak slots.
```json theme={null}
{
"chargingProfileId": null,
"transactionId": null,
"stackLevel": 1100,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": null,
"validFrom": null,
"validTo": null,
"chargingSchedule": {
"duration": 32400,
"startSchedule": "2026-04-14T22:00:00+00:00",
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 0.0, "numberPhases": 1 },
{ "startPeriod": 5400, "limit": 99999.0, "numberPhases": 1 },
{ "startPeriod": 27000, "limit": 0.0, "numberPhases": 1 }
],
"minChargingRate": 0.0
}
}
```
With `charging_mode: minimise_charging_time`, Axle charges for just long enough to meet the 10 kWh intent, starting at the beginning of the off-peak window. Axle adds a small safety factor on top to account for variable charging efficiency, so the scheduled energy will be slightly above the stated intent.
```json theme={null}
{
"chargingProfileId": null,
"transactionId": null,
"stackLevel": 1100,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": null,
"validFrom": null,
"validTo": null,
"chargingSchedule": {
"duration": 32400,
"startSchedule": "2026-04-14T22:00:00+00:00",
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 0.0, "numberPhases": 1 },
{ "startPeriod": 5400, "limit": 99999.0, "numberPhases": 1 },
{ "startPeriod": 10800, "limit": 0.0, "numberPhases": 1 }
],
"minChargingRate": 0.0
}
}
```
***
## Scenario 3: Intent larger than the off-peak window
The user's energy requirement cannot be met using the off-peak window alone.
| [Intent](/workflows/smart-charging/api-reference/intent) | Value |
| -------------------------------------------------------- | ------------------------------------ |
| Energy required | 45 kWh |
| Charging mode | `maximise_charging_time` *(default)* |
Axle uses every off-peak slot available but falls short of the 45 kWh intent. Peak slots are left idle because `may_charge_peak` is `false`.
```json theme={null}
{
"chargingProfileId": null,
"transactionId": null,
"stackLevel": 1100,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": null,
"validFrom": null,
"validTo": null,
"chargingSchedule": {
"duration": 32400,
"startSchedule": "2026-04-14T22:00:00+00:00",
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 0.0, "numberPhases": 1 },
{ "startPeriod": 5400, "limit": 99999.0, "numberPhases": 1 },
{ "startPeriod": 27000, "limit": 0.0, "numberPhases": 1 }
],
"minChargingRate": 0.0
}
}
```
With `may_charge_peak: true`, Axle fills the off-peak window and tops up with the earliest available peak slot to reach the 45 kWh intent.
```json theme={null}
{
"chargingProfileId": null,
"transactionId": null,
"stackLevel": 1100,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": null,
"validFrom": null,
"validTo": null,
"chargingSchedule": {
"duration": 32400,
"startSchedule": "2026-04-14T22:00:00+00:00",
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 99999.0, "numberPhases": 1 },
{ "startPeriod": 1800, "limit": 0.0, "numberPhases": 1 },
{ "startPeriod": 5400, "limit": 99999.0, "numberPhases": 1 },
{ "startPeriod": 27000, "limit": 0.0, "numberPhases": 1 }
],
"minChargingRate": 0.0
}
}
```
***
## Scenario 4: Mid-session re-scheduling
Axle may re-issue schedules during a session. Each new schedule fully replaces the previous one at the same stack level.
| [Intent](/workflows/smart-charging/api-reference/intent) | Value |
| -------------------------------------------------------- | ------------------------------------ |
| Energy required | 30 kWh |
| May charge at peak | `false` *(default)* |
| Charging mode | `maximise_charging_time` *(default)* |
At 00:30 a market change makes it more valuable for Axle to insert a one-hour pause, so Axle sends a revised schedule covering the remainder of the session. The Actual row shows what the charger actually delivers — the initial schedule applies until the update lands, and the revised schedule applies from then on.
```json theme={null}
{
"chargingProfileId": null,
"transactionId": null,
"stackLevel": 1100,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": null,
"validFrom": null,
"validTo": null,
"chargingSchedule": {
"duration": 54000,
"startSchedule": "2026-04-14T19:00:00+00:00",
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 0.0, "numberPhases": 1 },
{ "startPeriod": 16200, "limit": 99999.0, "numberPhases": 1 },
{ "startPeriod": 37800, "limit": 0.0, "numberPhases": 1 }
],
"minChargingRate": 0.0
}
}
```
```json theme={null}
{
"chargingProfileId": null,
"transactionId": null,
"stackLevel": 1100,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": null,
"validFrom": null,
"validTo": null,
"chargingSchedule": {
"duration": 34200,
"startSchedule": "2026-04-15T00:30:00+00:00",
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 0.0, "numberPhases": 1 },
{ "startPeriod": 3600, "limit": 99999.0, "numberPhases": 1 },
{ "startPeriod": 18000, "limit": 0.0, "numberPhases": 1 }
],
"minChargingRate": 0.0
}
}
```
***
## Scenario 5: No off-peak slots before ready-by
The user plugs in at 07:00 and needs the car ready by 11:00. There are no off-peak slots between now and the deadline.
| [Intent](/workflows/smart-charging/api-reference/intent) | Value |
| -------------------------------------------------------- | ------------------------------------ |
| Energy required | 10 kWh |
| Charging mode | `maximise_charging_time` *(default)* |
With `may_charge_peak: false`, Axle cannot use peak slots. With no off-peak slots available, Axle cannot deliver any energy toward the 10 kWh intent. We still send a schedule (a single 0 W period) to tell the charger explicitly not to charge.
If the user needs to override this and charge immediately, initiate the charge on your side and notify Axle with a [charge-now event](/api-reference/entities/asset/event/charge-now).
```json theme={null}
{
"chargingProfileId": null,
"transactionId": null,
"stackLevel": 1100,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": null,
"validFrom": null,
"validTo": null,
"chargingSchedule": {
"duration": 14400,
"startSchedule": "2026-04-15T07:00:00+00:00",
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 0.0, "numberPhases": 1 }
],
"minChargingRate": 0.0
}
}
```
With `may_charge_peak: true`, Axle charges at peak rate for just long enough to meet the 10 kWh intent, then idles.
```json theme={null}
{
"chargingProfileId": null,
"transactionId": null,
"stackLevel": 1100,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": null,
"validFrom": null,
"validTo": null,
"chargingSchedule": {
"duration": 14400,
"startSchedule": "2026-04-15T07:00:00+00:00",
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 99999.0, "numberPhases": 1 },
{ "startPeriod": 5400, "limit": 0.0, "numberPhases": 1 }
],
"minChargingRate": 0.0
}
}
```
***
## Scenario 6: Flat-rate tariff
The user is on a single-rate tariff, so every half-hour between plug-in and ready-by costs them the same.
| [Tariff](/workflows/smart-charging/api-reference/tariff) | Value |
| -------------------------------------------------------- | ------ |
| Off-peak times | `24/7` |
| [Intent](/workflows/smart-charging/api-reference/intent) | Value |
| -------------------------------------------------------- | ------------------------ |
| Energy required | 30 kWh |
| May charge at peak | *no effect — see below* |
| Charging mode | `minimise_charging_time` |
With the whole day off-peak, any period between plug-in and ready-by can be used for charging. Axle schedules 4.5 hours of charging which is enough to cover the 30 kWh intent. These 4.5 hours will be selected according to our energy flexibility optimisation and can vary depending on the specifics of that night. Here we show two charging blocks as an arbitrary example but on another night the algorithm might schedule the required charging time differently.
Because charging is scheduled in whole half-hours, the last block runs slightly past the point where the intent is met, which is where the marker sits. As in [Scenario 2](#scenario-2-minimising-energy-delivered), the scheduled energy under `minimise_charging_time` will be a little above the stated intent rather than exactly equal to it.
`may_charge_peak` makes no difference on a flat-rate tariff. There are no peak periods for it to unlock, so either value produces the same schedule.
```json theme={null}
{
"chargingProfileId": null,
"transactionId": null,
"stackLevel": 1100,
"chargingProfilePurpose": "TxProfile",
"chargingProfileKind": "Absolute",
"recurrencyKind": null,
"validFrom": null,
"validTo": null,
"chargingSchedule": {
"duration": 32400,
"startSchedule": "2026-04-14T22:00:00+00:00",
"chargingRateUnit": "W",
"chargingSchedulePeriod": [
{ "startPeriod": 0, "limit": 0.0, "numberPhases": 1 },
{ "startPeriod": 5400, "limit": 99999.0, "numberPhases": 1 },
{ "startPeriod": 12600, "limit": 0.0, "numberPhases": 1 },
{ "startPeriod": 16200, "limit": 99999.0, "numberPhases": 1 },
{ "startPeriod": 25200, "limit": 0.0, "numberPhases": 1 }
],
"minChargingRate": 0.0
}
}
```
***
## Interpreting schedules
A few things to know when handling the schedules Axle sends you.
Axle sends schedules at OCPP `stackLevel: 1100` with `chargingProfilePurpose: TxProfile`. You should install a long-running `TxDefaultProfile` at a lower stack level (e.g. `0`) representing your fallback charging policy. When Axle's `TxProfile` expires or is superseded, the charger falls back to your default rather than continuing an old Axle schedule.
It also acts as a failsafe: if the charger ever loses connectivity or hasn't yet received a schedule, it falls back to your default rather than charging at full rate immediately.
Axle's schedules at a given `stackLevel` fully replace the previous schedule at that level. Don't try to merge them. Treat the most recent schedule as authoritative.
An Axle schedule containing a single `chargingSchedulePeriod` with `limit: 0.0` is an explicit instruction: hold the charger at 0 W for the schedule's duration. It's not an empty or invalid payload. Apply it like any other schedule. The user can always supersede it by triggering an immediate overriding charge.
This happens when Axle cannot schedule any charging between the plug-in time and ready-by, typically when `may_charge_peak` is `false` and no off-peak slots are available, or when the intent is already satisfied.
***
## Next steps
Back to the full Smart Charging integration walkthrough
See the full JSON shape of a schedule Axle sends
# Webhooks
Source: https://docs.axle.energy/workflows/webhooks
Receive signed events from Axle when something happens to one of your users or assets
Often something will happen in Axle's system that you want to know about in your own system. For example, a user might finish onboarding and become eligible for dispatch. You can receive these events in real time via webhooks.
```mermaid theme={null}
sequenceDiagram
participant Axle
participant You as Your endpoint
Axle->>You: POST one event, signed with x-axle-sig
You->>You: Verify signature against raw body
alt Durably accepted
You-->>Axle: 2xx
Note over Axle,You: Delivered, nothing more to send
else Genuine refusal
You-->>Axle: 4xx
Note over Axle,You: Dropped for good, never retried
else Transient failure
You-->>Axle: 5xx, timeout or connection error
Axle->>Axle: Wait 30s, doubling to 4h
Axle->>You: Retry, same id and created_at
Note over Axle,You: Retries stop 24h after created_at
end
```
Messages are delivered at-least-once, and may arrive out of order, so your handler should be idempotent and sequence on `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](mailto:hello@axle.energy) 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 a `2xx` response once the event is durably accepted.
## Events
| Event type | Fires when |
| -------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `user:onboarding:complete` | A user finishes onboarding: enrolled, consented, and eligible for dispatch. |
| `asset:dispatch:requested` | A market dispatch is scheduled for one of your assets and we need you to execute it over the window. |
| `vpp:asset:registered` | A battery is registered on the Axle VPP through Axle's signup flow, and we hand you the identifiers for it. |
| `vpp:dispatch:requested` | A VPP grid event is scheduled and we need you to hold a target power on each affected battery over the window. |
The `vpp:` events are for battery fleets on the [Axle VPP](/workflows/axle-vpp/integration), 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 in `payload`, 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.
```json theme={null}
{
"id": "0198f0a1-2b3c-7d4e-8f90-1a2b3c4d5e6f",
"version": "v0.0.0",
"event_type": "user:onboarding:complete",
"created_at": "2026-08-11T14:02:11.482913Z",
"payload": {
"site_id": "3c361164-5187-4b4b-8e8a-7b3f67c4d5a8",
"completed_at": "2026-08-11T14:02:11.301045Z",
"external_user_id": "1234567"
}
}
```
Field order isn't part of the contract, so parse the JSON rather than matching on it. It is stable for a given event, including across retries, but don't rely on it matching the order documented here.
| Field | Type | Description |
| ------------ | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string (UUID) | Unique identifier for this event. Stable across retries, so use it to deduplicate. |
| `version` | string | Envelope version, set per event type. The `vpp:` events are at `v1.0.0`; the others stay at `v0.0.0` while their contracts settle. |
| `event_type` | string | Which event this is, from the table above. |
| `created_at` | string (ISO 8601, UTC) | When Axle recorded the event, which can be earlier than when we sent it. A retried event keeps its original `created_at`. |
| `payload` | object | Event-specific fields. |
## Event payloads
The `payload` 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`
| Field | Type | Description |
| -------------------------- | ---------------------- | ------------------------------------------------------------------------ |
| `payload.site_id` | string (UUID) | The Axle site identifier for this user. |
| `payload.external_user_id` | string | Your own identifier for the user, as supplied when the site was created. |
| `payload.completed_at` | string (ISO 8601, UTC) | When the user finished onboarding. |
### `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.
| Field | Type | Description |
| ------------------ | ---------------------- | ---------------------------------------------------------------- |
| `payload.asset_id` | string (UUID) | The Axle asset identifier for the asset being dispatched. |
| `payload.action` | string | What to do with the asset over the window, from the table below. |
| `payload.from_dt` | string (ISO 8601, UTC) | Start of the dispatch window. |
| `payload.to_dt` | string (ISO 8601, UTC) | End of the dispatch window. |
| Action | Meaning |
| ------------------- | ---------------------------------------------------------------------------- |
| `pause_consumption` | Drive the asset's consumption to **0%** of its rated power for the window. |
| `max_consumption` | Drive the asset's consumption to **100%** of its rated power for the window. |
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](/workflows/axle-vpp/api-reference/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.
| Field | Type | Description |
| ----------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------- |
| `payload.site_id` | string (UUID) | The Axle site identifier for the customer. |
| `payload.asset_id` | string (UUID) | The Axle asset identifier for the battery. Use this for telemetry and dispatch. |
| `payload.device_serial` | string | The device serial as returned by your own device-list API, so you can match the asset to your customer. |
| `payload.registered_at` | string (ISO 8601, UTC) | When Axle created the asset. |
```json theme={null}
{
"id": "0198f0b2-3c4d-7e5f-8a01-2b3c4d5e6f70",
"version": "v1.0.0",
"event_type": "vpp:asset:registered",
"created_at": "2026-08-11T14:02:11.482913Z",
"payload": {
"site_id": "9f2b7a13-4c8d-4e21-9a76-5d1c3b8e0f42",
"asset_id": "3c361164-5187-4b4b-8e8a-7b3f67c4d5a8",
"device_serial": "OEM-SERIAL-12345",
"registered_at": "2026-08-11T14:02:11.301045Z"
}
}
```
See [Axle VPP integration](/workflows/axle-vpp/integration) for where this sits in the wider signup flow.
### `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`.
| Field | Type | Description |
| --------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `payload.event_id` | string (UUID) | Identifier for the grid event itself, shared by every asset in this instruction. Distinct from the envelope `id`, which identifies this delivery. |
| `payload.issued_timestamp` | string (ISO 8601, UTC) | When Axle issued the instruction. |
| `payload.start_time` | string (ISO 8601, UTC) | Start of the dispatch window. |
| `payload.end_time` | string (ISO 8601, UTC) | End of the dispatch window. |
| `payload.assets` | array | One entry per affected asset. |
| `payload.assets[].asset_id` | string (UUID) | The Axle asset identifier, as supplied at registration. |
| `payload.assets[].power_kw` | number | Target power for the asset across the window, applied at the inverter. Positive = charge (import into the battery), negative = discharge (export). |
```json theme={null}
{
"id": "0198f0c3-4d5e-7f60-9b12-3c4d5e6f7081",
"version": "v1.0.0",
"event_type": "vpp:dispatch:requested",
"created_at": "2026-06-29T17:02:37.512804Z",
"payload": {
"event_id": "eab8c867-7c4d-4116-a587-02c9d1f13a16",
"issued_timestamp": "2026-06-29T17:02:37.448917+00:00",
"start_time": "2026-06-30T20:30:00+00:00",
"end_time": "2026-06-30T21:30:00+00:00",
"assets": [
{ "asset_id": "3c361164-5187-4b4b-8e8a-7b3f67c4d5a8", "power_kw": -6.915 },
{ "asset_id": "1a161164-5187-4b4b-8e8a-7b3f67c4d5a8", "power_kw": -4.915 }
]
}
}
```
This event is batched, so a `4xx` refuses the whole instruction and we won't retry any part of it. If you don't recognise one `asset_id`, skip that entry and still return a `2xx`. Return a `5xx` only if you can't accept the instruction at all.
Deduplicate on the envelope `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:
| Header | Description |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `x-axle-sig` | `t=,v1=`. We cover the timestamp in the signature, so a captured request can't be replayed indefinitely. |
| `x-axle-ref` | The envelope `id`, for correlation in your logs. Not signed, so don't rely on it for anything security-sensitive. |
Verify in four steps; both samples below do all of them.
Verify against the exact bytes we sent. Parsing the JSON and re-serialising it will change the
signature.
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.
Discard the request if `t` is more than 300 seconds from your clock. This is what bounds the
replay window.
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 theme={null}
import hashlib
import hmac
TOLERANCE_SEC = 300
def verify(secret: str, body: str, signature_header: str, received_at_ts: int) -> bool:
fields = dict(f.split("=", 1) for f in signature_header.split(",") if "=" in f)
timestamp, signature = fields.get("t"), fields.get("v1")
# isdigit() alone accepts non-ASCII digits that int() then rejects, so check both
if not timestamp or not signature or not (timestamp.isascii() and timestamp.isdigit()):
return False
if abs(received_at_ts - int(timestamp)) > TOLERANCE_SEC:
return False
expected = hmac.new(secret.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256).hexdigest()
# compare as bytes: compare_digest rejects str containing non-ASCII
return hmac.compare_digest(expected.encode(), signature.encode())
```
```javascript theme={null}
const crypto = require("crypto");
const TOLERANCE_SEC = 300;
function verify(secret, body, signatureHeader, receivedAtTs) {
const fields = Object.fromEntries(
signatureHeader.split(",").map((field) => {
const i = field.indexOf("=");
return [field.slice(0, i), field.slice(i + 1)];
}),
);
const { t, v1 } = fields;
if (!/^\d+$/.test(t ?? "") || !v1) return false;
if (Math.abs(receivedAtTs - Number(t)) > TOLERANCE_SEC) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(v1, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```
### Test vector
Check your implementation against this before you go live. Note the compact JSON, with no spaces between tokens.
| | |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Secret | `axle_whsec_example` |
| Body | `{"id":"0198f0a1-2b3c-7d4e-8f90-1a2b3c4d5e6f","version":"v0.0.0","event_type":"user:onboarding:complete","created_at":"2026-08-11T14:02:11.482913Z","payload":{"site_id":"3c361164-5187-4b4b-8e8a-7b3f67c4d5a8","completed_at":"2026-08-11T14:02:11.301045Z","external_user_id":"1234567"}}` |
| `x-axle-sig` | `t=1786456932,v1=d4554b1fccf035c008a8040d20ad5a6c8e6f45299371945c7895084bf7300211` |
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.
If we're part-way through rotating your secret you may hold two at once. Accept a request that
verifies against either, and drop the old one when we confirm the rotation is done.
## Responding
Return any `2xx` 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.
| Your response | What we do |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `2xx` | Mark the delivery succeeded. Nothing more to do. |
| `4xx` | Treat it as a definitive refusal and stop. A malformed request or the wrong endpoint won't improve with time. |
| `5xx`, timeout, connection error | Try again with backoff. See [Retries](#retries). |
A `4xx` drops the event for good. If your handler can't accept it right now, because a dependency is
down for example, return a `5xx` and we'll come back to it.
## 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's `created_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 on `created_at` or 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_at` and 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-sig` verified 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_id`s skipped rather than refusing the whole instruction.
* [ ] `2xx` returned only once the event is durably accepted, `5xx` on transient failure, and `4xx` only for a genuine refusal.
* [ ] Signing secrets stored as secrets, and kept distinct per environment.
If you hit anything that isn't covered here, [get in touch](mailto:hello@axle.energy). We want to make sure this documentation is complete and correct, and that your integration works smoothly.
## Next steps
Where onboarding fits in the wider integration
Where the `vpp:` events fit in a battery fleet integration