Airpipe Connect API
One REST API to link, read and control every energy device. Base URL https://connect.airpipe.ai/api. JSON in, JSON out.
Introduction
The Connect API exposes a small, normalized surface over dozens of vendor clouds and protocols. You link an end-user's vendor account once, then read a single normalized Device shape and send vendor-agnostic Commands — the same across EVs, chargers, heat pumps, AC, solar, batteries, appliances and lighting.
All requests are made over HTTPS to the base URL below. Request and response bodies are JSON.
Endpoints at a glance
| Method | Path | Purpose |
|---|---|---|
| POST | /link | Link a vendor account, register its devices |
| GET | /devices | List the consumer's linked devices |
| GET | /devices/{id} | One device with its latest state |
| POST | /devices/{id}/command | Apply a normalized command |
| POST | /partners/apply | Public partner application intake |
| POST | /webhooks/{vendor} | Vendor push webhook |
| GET | /oauth/{vendor}/authorize | Start vendor OAuth (redirect) |
| GET | /oauth/{vendor}/callback | OAuth callback (code → vault) |
Authentication
Every management request is authenticated by a consumer API key. The consumer identity is derived from the verified key on the server — never from a client-set header. Present the key as a bearer token:
curl https://connect.airpipe.ai/api/devices \
-H "Authorization: Bearer sk_live_…"Alternatively, pass the key in the X-Api-Key header:
curl https://connect.airpipe.ai/api/devices \
-H "X-Api-Key: sk_live_…"Scopes
Keys carry scopes. Each endpoint requires exactly one:
| Scope | Grants |
|---|---|
link:write | Link an account and register devices (POST /link) |
devices:read | List devices and read state (GET /devices, GET /devices/{id}) |
devices:command | Send commands (POST /devices/{id}/command) |
401 Unauthorized. A valid key lacking the required scope returns 403. An over-quota key returns 429 Too Many Requests.Devices
Link a vendor account, then list and inspect the devices it exposes.
Link an account
Provide the vendor slug, a consumer-local user_ref identifying the end user, and adapter-specific credentials. The response lists the devices discovered and linked.
curl -X POST https://connect.airpipe.ai/api/link \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"vendor": "easee",
"user_ref": "user_123",
"credentials": { "username": "…", "password": "…" }
}'
{
"devices": [
{ "id": "charger_9f2a", "vendor": "easee", "device_type": "charger" }
]
}List devices
curl https://connect.airpipe.ai/api/devices \
-H "Authorization: Bearer sk_live_…"
[
{
"id": "charger_9f2a",
"vendor": "easee",
"device_type": "charger",
"name": "Garage",
"capabilities": ["read_power", "set_current", "load_balancing"]
}
]Get one device with state
{
"device": {
"id": "charger_9f2a",
"vendor": "easee",
"device_type": "charger",
"name": "Garage",
"capabilities": ["read_power", "read_energy", "set_current", "load_balancing"]
},
"state": {
"power_w": 7360,
"energy_wh": 12840,
"connected": true,
"charging": true,
"online": true
}
}Device model
Every device is normalized to the same shape regardless of vendor.
Device
| Field | Type | Description |
|---|---|---|
id | string | Vendor device id, unique within (consumer, vendor) |
vendor | string | Lower-case vendor slug, e.g. easee, tesla |
device_type | enum | vehicle, charger, heat_pump, thermostat, solar_inverter, battery, smart_home_appliance, light, elevator, industrial |
name | string? | Human-readable name when the vendor supplies one |
capabilities | string[] | The capability set the adapter guarantees (see below) |
Capabilities
Commands and reads are gated by capabilities, not by device type.
| Capability | Meaning |
|---|---|
read_battery_soc | Battery state-of-charge in percent (vehicle path only) |
read_power | Signed active power in watts |
read_energy | Session/meter energy in watt-hours |
read_temperature | Temperature in milli-Celsius |
read_production | Production power in watts |
on_off | Can be switched on and off |
charge_limit | Accepts a charge target limit (percent) |
charge_control | Can start and stop charging |
set_current | Charge current limit in whole amperes (0 = pause) |
load_balancing | First-class current/power limiting against a fuse/grid cap |
set_temperature | Accepts a target temperature |
set_light | Accepts dim/colour control |
set_battery_power | Accepts a (dis)charge power setpoint |
program_control | Start/stop an appliance program |
delayed_start | Schedule a delayed program start |
DeviceState
Every field is optional — an adapter fills only values backed by a supported read capability.
| Field | Type | Description |
|---|---|---|
battery_soc | int? | State-of-charge percent (vehicles; not OCPP chargers) |
power_w | int? | Signed active power, watts |
energy_wh | int? | Session/meter energy, watt-hours |
temperature_milli_c | int? | Temperature, milli-Celsius |
production_w | int? | Production power, watts |
connected | bool? | Vehicle/cable connected to charger |
charging | bool? | Energy actively transferring |
online | bool | Device currently reachable |
power_on | bool? | Powered on (white goods / smart home) |
operation_state | string? | Vendor state label, e.g. ready, run |
remaining_seconds | int? | Remaining program time |
door_open | bool? | Appliance door open |
energy_wh / power_w and connection flags, not SoC.Commands
Send one normalized command. The body is a tagged union keyed by type. Each command maps to exactly one capability; if the device does not advertise it, the request is rejected with 422. The response is the device with its refreshed state.
curl -X POST https://connect.airpipe.ai/api/devices/charger_9f2a/command \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{ "type": "set_current", "current_a": 10 }'Command types
| type | Fields | Requires capability |
|---|---|---|
turn_on | — | on_off |
turn_off | — | on_off |
set_charge_limit | percent | charge_limit |
start_charge | — | charge_control |
stop_charge | — | charge_control |
set_current | current_a (0 = pause) | set_current |
set_temperature | target_milli_c | set_temperature |
set_light | dim, color [r,g,b] | set_light |
set_battery_power | power_w (signed) | set_battery_power |
start_program | — | program_control |
stop_program | — | program_control |
delayed_start | delay_seconds | delayed_start |
21000 = 21 °C). An adapter that lacks a capability rejects the command — never a silent no-op.Load balancing
Load balancing is a first-class capability. Every controllable device that can constrain its own draw against a fuse or grid limit advertises load_balancing — a guaranteed, tested interface rather than per-vendor plumbing. A device may limit by current, by power, or both; the unsupported axis is cleanly rejected.
curl -X POST https://connect.airpipe.ai/api/devices/charger_9f2a/command \
-H "Authorization: Bearer sk_live_…" \
-d '{ "type": "set_current", "current_a": 6 }'Load balancers expose a static limit envelope and current headroom:
| Concept | Description |
|---|---|
limits | Static envelope: min_a/max_a and/or min_w/max_w. Unsupported axes are null. |
available_capacity | Current headroom the circuit can accept — amperes and/or watts. |
set_current | Amperes-based limit (wallbox pattern). 0 A pauses. |
set_battery_power | Watts-based limit (heat-pump / battery pattern). |
Webhooks
Vendors that push state changes call this endpoint. It accepts and acknowledges the delivery with 202 Accepted. Signature verification and state ingestion are vendor-specific.
curl -X POST https://connect.airpipe.ai/api/webhooks/easee \
-H "Content-Type: application/json" \
-d '{ "event": "charger.state", "id": "charger_9f2a" }'
202 AcceptedOAuth linking
For vendors that use OAuth (e.g. Tesla), start the hosted authorization flow and let the user consent with the vendor. On success the callback exchanges the code and vaults the tokens — credentials never touch your servers.
# Redirect the user to:
https://connect.airpipe.ai/api/oauth/tesla/authorize
# Vendor redirects back to the callback, which vaults tokens:
https://connect.airpipe.ai/api/oauth/tesla/callback?code=…&state=…client_id/client_secret, a hosted public key at /.well-known/appspecific/com.tesla.3p.public-key.pem, and user consent.Optimize
The smart-charging planner. Give it a list of forward electricity prices and a charge task, and it returns the cheapest schedule that respects the device's power envelope and deadline. Money fields are decimal strings; energy and power are whole integers in Wh/W.
Each entry in prices is a slot with a duration_min and a price (a decimal string or number). The task describes the battery state and the power limits. device_profile is optional — when supplied, the response also includes ready-to-send commands per slot.
Request
| Field | Type | Description |
|---|---|---|
prices[].duration_min | int | Slot length in minutes |
prices[].price | string|number | Price for the slot (decimal string or number) |
task.current_soc | int | Current state-of-charge, percent |
task.target_soc | int | Desired state-of-charge, percent |
task.capacity_wh | int | Usable battery capacity, watt-hours |
task.max_power_w | int | Max charge power, watts |
task.min_power_w | int | Min charge power when active, watts |
task.deadline_slot_index | int | Charging must complete by this slot index |
device_profile | object? | Optional: kind, voltage_v, phases — enables per-slot commands |
curl -X POST https://connect.airpipe.ai/api/optimize/plan \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"prices": [
{ "duration_min": 60, "price": "0.50" },
{ "duration_min": 60, "price": "1.50" },
{ "duration_min": 60, "price": "0.90" }
],
"task": {
"current_soc": 20,
"target_soc": 80,
"capacity_wh": 60000,
"max_power_w": 11000,
"min_power_w": 0,
"deadline_slot_index": 2
},
"device_profile": { "kind": "charger", "voltage_v": 230, "phases": 1 }
}'
{
"slots": [
{ "index": 0, "power_w": 11000, "expected_energy_wh": 11000, "price": "0.50" },
{ "index": 1, "power_w": 11000, "expected_energy_wh": 11000, "price": "1.50" },
{ "index": 2, "power_w": 11000, "expected_energy_wh": 11000, "price": "0.90" }
],
"total_energy_wh": 33000,
"total_cost": "44.00",
"target_reachable": false,
"commands": [
{ "slot_index": 0, "command": { "type": "set_current", "current_a": 48 } }
]
}Response
| Field | Type | Description |
|---|---|---|
slots[].index | int | Slot index, aligned to the request price list |
slots[].power_w | int | Planned charge power for the slot, watts |
slots[].expected_energy_wh | int | Energy delivered in the slot, watt-hours |
slots[].price | string | Slot price, decimal string |
total_energy_wh | int | Total planned energy, watt-hours |
total_cost | string | Total cost, decimal string |
target_reachable | bool | false when the target SoC cannot be met by the deadline |
commands | array? | Per-slot device commands — present only when device_profile is supplied |
target_reachable is false when the deadline and power envelope cannot reach target_soc — the planner returns the best achievable schedule rather than a false success. Invalid input returns 400 { "error": … }; a missing or invalid API key returns 401.Flex
Fleet-level demand flexibility. Dispatch an up- or down-regulation request across a fleet, or query how much flexibility the fleet can offer for a period. As everywhere in Connect, power is in whole watts and the response is honest about what was actually delivered.
Dispatch
Request a change in aggregate draw. direction is "down" (shed load — reduce power) or "up" (absorb — increase power) by amount_w. Each fleet member declares a baseline_w, its controllable range min_w/max_w, and a priority. Devices are recruited in ascending priority order until the request is met.
curl -X POST https://connect.airpipe.ai/api/flex/dispatch \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"fleet": [
{ "id": "charger_a", "baseline_w": 5000, "min_w": 1000, "max_w": 7000, "priority": 1 }
],
"request": { "direction": "down", "amount_w": 4000 }
}'
{
"setpoints": [
{ "device_id": "charger_a", "target_w": 4000, "delta_w": -1000 }
],
"delivered_w": 4000,
"shortfall_w": 0
}| Field | Type | Description |
|---|---|---|
setpoints[].device_id | string | Fleet member id |
setpoints[].target_w | int | New absolute power setpoint, watts |
setpoints[].delta_w | int | Change from baseline, signed watts |
delivered_w | int | Regulation actually delivered, watts |
shortfall_w | int | Requested minus delivered — 0 when fully met |
delivered_w reports what was achievable and shortfall_w the unmet remainder — never an overstated success.Flexibility
Ask how much up- and down-regulation the fleet can offer for a period, relative to its baseline draw.
curl -X POST https://connect.airpipe.ai/api/flex/flexibility \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"fleet": [
{ "id": "charger_a", "baseline_w": 5000, "min_w": 1000, "max_w": 7000, "priority": 1 },
{ "id": "charger_b", "baseline_w": 5000, "min_w": 500, "max_w": 7500, "priority": 2 }
],
"period": { "index": 0, "duration_min": 15 }
}'
{
"up_w": 5000,
"down_w": 9500,
"baseline_w": 10000,
"period_index": 0
}| Field | Type | Description |
|---|---|---|
up_w | int | Headroom to increase draw above baseline, watts |
down_w | int | Headroom to reduce draw below baseline, watts |
baseline_w | int | Aggregate fleet baseline, watts |
period_index | int | Echoed period index |
400 { "error": … }, and a missing or invalid API key returns 401.Errors
Errors use standard HTTP status codes with a JSON body of the shape { "error": "…" }.
{ "error": "missing required scope: devices:command" }| Status | Meaning |
|---|---|
400 Bad Request | Malformed request or unknown vendor |
401 Unauthorized | Missing or invalid API key |
403 Forbidden | Authenticated but lacking the required scope |
404 Not Found | Referenced device does not exist |
422 Unprocessable Entity | Device does not support the command's capability |
429 Too Many Requests | Rate limit exceeded for the API key |
502 Bad Gateway | Upstream vendor/adapter failure |
500 Internal Server Error | Unexpected server error |