API Reference

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.

Basehttps://connect.airpipe.ai/api

Endpoints at a glance

MethodPathPurpose
POST/linkLink a vendor account, register its devices
GET/devicesList the consumer's linked devices
GET/devices/{id}One device with its latest state
POST/devices/{id}/commandApply a normalized command
POST/partners/applyPublic partner application intake
POST/webhooks/{vendor}Vendor push webhook
GET/oauth/{vendor}/authorizeStart vendor OAuth (redirect)
GET/oauth/{vendor}/callbackOAuth 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:

Authorization header
curl https://connect.airpipe.ai/api/devices \
  -H "Authorization: Bearer sk_live_…"

Alternatively, pass the key in the X-Api-Key header:

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:

ScopeGrants
link:writeLink an account and register devices (POST /link)
devices:readList devices and read state (GET /devices, GET /devices/{id})
devices:commandSend commands (POST /devices/{id}/command)
Failure modes. A missing or invalid key returns 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

POST/linklink:write

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.

POST /link
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

GET/devicesdevices:read
GET /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

GET/devices/{id}devices:read
GET /devices/charger_9f2a
{
  "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

FieldTypeDescription
idstringVendor device id, unique within (consumer, vendor)
vendorstringLower-case vendor slug, e.g. easee, tesla
device_typeenumvehicle, charger, heat_pump, thermostat, solar_inverter, battery, smart_home_appliance, light, elevator, industrial
namestring?Human-readable name when the vendor supplies one
capabilitiesstring[]The capability set the adapter guarantees (see below)

Capabilities

Commands and reads are gated by capabilities, not by device type.

CapabilityMeaning
read_battery_socBattery state-of-charge in percent (vehicle path only)
read_powerSigned active power in watts
read_energySession/meter energy in watt-hours
read_temperatureTemperature in milli-Celsius
read_productionProduction power in watts
on_offCan be switched on and off
charge_limitAccepts a charge target limit (percent)
charge_controlCan start and stop charging
set_currentCharge current limit in whole amperes (0 = pause)
load_balancingFirst-class current/power limiting against a fuse/grid cap
set_temperatureAccepts a target temperature
set_lightAccepts dim/colour control
set_battery_powerAccepts a (dis)charge power setpoint
program_controlStart/stop an appliance program
delayed_startSchedule a delayed program start

DeviceState

Every field is optional — an adapter fills only values backed by a supported read capability.

FieldTypeDescription
battery_socint?State-of-charge percent (vehicles; not OCPP chargers)
power_wint?Signed active power, watts
energy_whint?Session/meter energy, watt-hours
temperature_milli_cint?Temperature, milli-Celsius
production_wint?Production power, watts
connectedbool?Vehicle/cable connected to charger
chargingbool?Energy actively transferring
onlineboolDevice currently reachable
power_onbool?Powered on (white goods / smart home)
operation_statestring?Vendor state label, e.g. ready, run
remaining_secondsint?Remaining program time
door_openbool?Appliance door open
SoC vs energy. Battery state-of-charge comes from the vehicle path — a pure OCPP charger reports energy_wh / power_w and connection flags, not SoC.

Commands

POST/devices/{id}/commanddevices:command

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.

POST /devices/charger_9f2a/command
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

typeFieldsRequires capability
turn_onon_off
turn_offon_off
set_charge_limitpercentcharge_limit
start_chargecharge_control
stop_chargecharge_control
set_currentcurrent_a (0 = pause)set_current
set_temperaturetarget_milli_cset_temperature
set_lightdim, color [r,g,b]set_light
set_battery_powerpower_w (signed)set_battery_power
start_programprogram_control
stop_programprogram_control
delayed_startdelay_secondsdelayed_start
All amperes and watts are whole integers; temperatures are milli-Celsius (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.

Set a 6 A circuit limit
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:

ConceptDescription
limitsStatic envelope: min_a/max_a and/or min_w/max_w. Unsupported axes are null.
available_capacityCurrent headroom the circuit can accept — amperes and/or watts.
set_currentAmperes-based limit (wallbox pattern). 0 A pauses.
set_battery_powerWatts-based limit (heat-pump / battery pattern).
No silent no-ops. An amperes-only device rejects a power limit with a clear message, and vice versa.

Webhooks

POST/webhooks/{vendor}

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.

POST /webhooks/easee
curl -X POST https://connect.airpipe.ai/api/webhooks/easee \
  -H "Content-Type: application/json" \
  -d '{ "event": "charger.state", "id": "charger_9f2a" }'

202 Accepted

OAuth 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.

GET/oauth/{vendor}/authorize
GET/oauth/{vendor}/callback
Start Tesla OAuth
# 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=…
Tesla requires partner 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.

POST/optimize/plan

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

FieldTypeDescription
prices[].duration_minintSlot length in minutes
prices[].pricestring|numberPrice for the slot (decimal string or number)
task.current_socintCurrent state-of-charge, percent
task.target_socintDesired state-of-charge, percent
task.capacity_whintUsable battery capacity, watt-hours
task.max_power_wintMax charge power, watts
task.min_power_wintMin charge power when active, watts
task.deadline_slot_indexintCharging must complete by this slot index
device_profileobject?Optional: kind, voltage_v, phases — enables per-slot commands
POST /optimize/plan
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

FieldTypeDescription
slots[].indexintSlot index, aligned to the request price list
slots[].power_wintPlanned charge power for the slot, watts
slots[].expected_energy_whintEnergy delivered in the slot, watt-hours
slots[].pricestringSlot price, decimal string
total_energy_whintTotal planned energy, watt-hours
total_coststringTotal cost, decimal string
target_reachableboolfalse when the target SoC cannot be met by the deadline
commandsarray?Per-slot device commands — present only when device_profile is supplied
Honest planning. 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

POST/flex/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.

POST /flex/dispatch
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
}
FieldTypeDescription
setpoints[].device_idstringFleet member id
setpoints[].target_wintNew absolute power setpoint, watts
setpoints[].delta_wintChange from baseline, signed watts
delivered_wintRegulation actually delivered, watts
shortfall_wintRequested minus delivered — 0 when fully met
Honest delivery. If the fleet's range cannot cover the request, delivered_w reports what was achievable and shortfall_w the unmet remainder — never an overstated success.

Flexibility

POST/flex/flexibility

Ask how much up- and down-regulation the fleet can offer for a period, relative to its baseline draw.

POST /flex/flexibility
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
}
FieldTypeDescription
up_wintHeadroom to increase draw above baseline, watts
down_wintHeadroom to reduce draw below baseline, watts
baseline_wintAggregate fleet baseline, watts
period_indexintEchoed period index
Both Flex endpoints validate their input: malformed bodies return 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 body
{ "error": "missing required scope: devices:command" }
StatusMeaning
400 Bad RequestMalformed request or unknown vendor
401 UnauthorizedMissing or invalid API key
403 ForbiddenAuthenticated but lacking the required scope
404 Not FoundReferenced device does not exist
422 Unprocessable EntityDevice does not support the command's capability
429 Too Many RequestsRate limit exceeded for the API key
502 Bad GatewayUpstream vendor/adapter failure
500 Internal Server ErrorUnexpected server error