Connect an OCPP charger
Every charger Ampr controls, whatever the manufacturer, goes through the same three steps: register it, wait for it to connect over OCPP, then control it with plain REST calls. This guide walks through that flow for a brand-new charger and shows what’s happening on the wire while you wait. For the broader pitch on what Ampr’s OCPP charger management layer covers, see that page.
All requests reuse the Authorization: Bearer $AMPR_API_KEY header from
the quickstart. Every call below is a real request
against https://api.ampr.dev/v1.
Register a charger for any vendor
POST /chargers only needs three fields: serial, vendor, and model.
Ampr doesn’t care which manufacturer made it, as long as the hardware
speaks OCPP 1.6 or 2.0.1, the same endpoint registers it:
curl https://api.ampr.dev/v1/chargers \
-H "Authorization: Bearer $AMPR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"serial": "ALF-114",
"vendor": "Alfen",
"model": "Eve Single Pro-line"
}' const res = await fetch('https://api.ampr.dev/v1/chargers', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.AMPR_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
serial: 'ALF-114',
vendor: 'Alfen',
model: 'Eve Single Pro-line',
}),
});
const { data: charger } = await res.json();
console.log(charger.id, charger.status, charger.claim_expires_at);
// chg_5xQ2wPk8Rv "pending" "2026-07-16T12:00:00Z" {
"data": {
"id": "chg_5xQ2wPk8Rv",
"serial": "ALF-114",
"vendor": "Alfen",
"model": "Eve Single Pro-line",
"firmware_version": null,
"status": "pending",
"location_id": null,
"tariff_id": null,
"claim_expires_at": "2026-07-16T12:00:00Z",
"sockets": [],
"created_at": "2026-07-14T12:00:00Z"
}
}
status starts at pending and sockets is empty, Ampr knows the charger
exists but hasn’t heard from it yet. claim_expires_at is 48 hours out: if
the physical unit never dials in and completes its OCPP handshake in that
window, the claim lapses. Point the charger’s OCPP backend URL at your
Ampr endpoint (from the dashboard under Settings → Chargers) and power
it on to start that handshake.
Optionally pass location_id or tariff_id at registration to bind the
charger to a site or a specific price; both can also be set later with
PATCH /chargers/{charger_id}.
Inspect its sockets
A charger isn’t one state machine, each physical connector is. Once the
charger connects and reports in, GET /chargers/{charger_id}/sockets
returns every connector on it independently:
curl https://api.ampr.dev/v1/chargers/chg_5xQ2wPk8Rv/sockets \
-H "Authorization: Bearer $AMPR_API_KEY" const res = await fetch(
'https://api.ampr.dev/v1/chargers/chg_5xQ2wPk8Rv/sockets',
{
headers: {
Authorization: `Bearer ${process.env.AMPR_API_KEY}`,
},
}
);
const { data: sockets } = await res.json();
console.log(sockets.map((s) => [s.connector_id, s.status]));
// [[1, "Available"], [2, "Available"]] {
"data": [
{
"id": "skt_7mR3pQw1",
"connector_id": 1,
"type": "Type2",
"status": "Available",
"current_session_id": null
},
{
"id": "skt_9kLp4Tn2",
"connector_id": 2,
"type": "Type2",
"status": "Available",
"current_session_id": null
}
]
}
A dual-socket charger like this one gets two independent skt_-prefixed
ids, and each carries its own status. One connector charging a vehicle
never blocks or misreports the other. Fetch a single connector directly
once you have its id:
curl https://api.ampr.dev/v1/chargers/chg_5xQ2wPk8Rv/sockets/skt_7mR3pQw1 \
-H "Authorization: Bearer $AMPR_API_KEY" const res = await fetch(
'https://api.ampr.dev/v1/chargers/chg_5xQ2wPk8Rv/sockets/skt_7mR3pQw1',
{
headers: {
Authorization: `Bearer ${process.env.AMPR_API_KEY}`,
},
}
);
const { data: socket } = await res.json();
console.log(socket.status, socket.current_session_id);
// "Available" null status is always one of Available, Preparing, Charging,
SuspendedEV, SuspendedEVSE, Finishing, Faulted, or Unavailable,
regardless of which OCPP version the charger speaks. current_session_id
points at the active ses_ session once one starts, and is null
otherwise.
Send remote commands
Control calls target either the charger (commands that affect the whole unit) or a specific socket (commands scoped to one connector). Reset the whole charger:
curl -X POST "https://api.ampr.dev/v1/chargers/chg_5xQ2wPk8Rv/reset?wait=true" \
-H "Authorization: Bearer $AMPR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "Soft"}' const res = await fetch(
'https://api.ampr.dev/v1/chargers/chg_5xQ2wPk8Rv/reset?wait=true',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.AMPR_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ type: 'Soft' }),
}
);
const { data: command } = await res.json();
console.log(command.type, command.status);
// "Reset" "accepted" type is Soft (restart software, connectors keep any active sessions
where possible) or Hard (power-cycle, always interrupts active
sessions). To act on a single connector instead, target its socket, unlock
releases a stuck cable:
curl -X POST "https://api.ampr.dev/v1/chargers/chg_5xQ2wPk8Rv/sockets/skt_7mR3pQw1/unlock?wait=true" \
-H "Authorization: Bearer $AMPR_API_KEY" const res = await fetch(
'https://api.ampr.dev/v1/chargers/chg_5xQ2wPk8Rv/sockets/skt_7mR3pQw1/unlock?wait=true',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.AMPR_API_KEY}`,
},
}
);
const { data: command } = await res.json();
console.log(command.type, command.status);
// "UnlockConnector" "accepted" Every command returns a cmd_-prefixed Command resource with a status
of pending, accepted, rejected, or timeout. wait=true blocks (up
to timeout seconds, default 30, max 60) until the charger acknowledges,
so you get the final status in one round trip; omit it for a 202 with
status: "pending" immediately, then poll the command or listen for its
webhook.
Sockets also take start and stop (covered in the
quickstart), plus charging-profile and
clear-charging-profile for Dynamic Load Balancing. Chargers additionally
take firmware, for pushing an OTA update and reading back
FirmwareStatusNotification progress.
OCPP 1.6 and 2.0.1, one API
Real fleets mix hardware generations, older units on OCPP 1.6, newer ones on 2.0.1, sometimes both on the same site. Ampr’s gateway detects which dialect a charger speaks at connection time and terminates it there, so none of the calls above change based on protocol version.
The two versions don’t always model state the same way. OCPP 1.6 reports
connector status as a flat string that lines up directly with the status
values you saw above. OCPP 2.0.1’s ConnectorStatusEnum is a different,
smaller vocabulary, notably Occupied, which only means a cable is plugged
in, not that current is flowing. Ampr normalizes it onto the same status
set rather than inventing a Charging you didn’t earn: Occupied maps to
Finishing (connected, non-drawing), Reserved maps to Unavailable, and
so on. Whichever protocol version is on the wall, GET /chargers/{charger_id}/sockets/{socket_id} gives you the same eight-value
enum to build against.
The first StatusNotification
Here’s what happens between registering a charger and its sockets showing
up. When the physical unit powers on and completes its OCPP handshake, it
sends a BootNotification, that’s what flips status from pending to
online and clears claim_expires_at back to null. The claim window
only covers the initial handshake, so once the charger is online there
is nothing left to expire.
Immediately after, it sends one StatusNotification per physical
connector announcing its initial state, almost always Available. That’s
the message that actually creates the rows you fetched above:
curl https://api.ampr.dev/v1/chargers/chg_5xQ2wPk8Rv/sockets/skt_7mR3pQw1 \
-H "Authorization: Bearer $AMPR_API_KEY" const res = await fetch(
'https://api.ampr.dev/v1/chargers/chg_5xQ2wPk8Rv/sockets/skt_7mR3pQw1',
{
headers: {
Authorization: `Bearer ${process.env.AMPR_API_KEY}`,
},
}
);
const { data: socket } = await res.json();
console.log(socket.status);
// "Available" From here, every later StatusNotification, someone plugs in
(Preparing), a session starts drawing (Charging), a fault trips
(Faulted), updates that same status field in place. It’s the same
lifecycle whether the charger reconnects after a network blip or you just
finished the registration flow above.
Don’t want to poll a socket to catch that first transition? The
handle webhooks guide covers
charger.connected, fired on every BootNotification including this
first one, and socket.status_changed, fired the moment a socket’s
status lands, so your integration finds out the instant a charger comes
online instead of on your next poll. charger.online is a related event,
but it’s reserved for later reconnects, a charger that goes offline
and then boots again, and won’t fire on this first connection.