ampr docs

IDs, errors, and versioning

Every other guide shows you the happy path: register a charger, start a session, get paid. This one covers the parts that don’t change from call to call, the shape of an id, the shape of an error, and what “stable” actually means for this API. If you’re writing an integration that’s meant to outlive your first afternoon with it, this is what to build against.

Resource IDs

Every id Ampr returns is a resource-type prefix, an underscore, then an opaque, URL-safe string. The prefix is part of the API contract, it never changes for a given resource, so you can use it to route or validate without a lookup:

ResourcePrefixExample
Chargerchg_chg_2kT8v9xBn4
Socketskt_skt_7mR3pQw1
Sessionses_ses_9nL5tYz2
Commandcmd_cmd_4jK8rWx6
CDR (charge detail record)cdr_cdr_1hF3mNv8
Payment referencepyr_pyr_1hF3mNv8
Locationloc_loc_5cA4hLx7
Tarifftrf_trf_2bD6fMy3
Token (RFID/app credential)trk_trk_6pS2qUy4
Webhookwhk_whk_6pS2qUy4
Organizationorg_org_7cX2wQa9
API keykey_key_...b9x4 (masked; the full value is only ever shown once)
User (dashboard account)usr_usr_2kT8v9xBn4
Fetch a charger by id
curl https://api.ampr.dev/v1/chargers/chg_2kT8v9xBn4 \
-H "Authorization: Bearer $AMPR_API_KEY"

Every id in that table is what its own endpoint actually returns, not a display convention layered on top of something else. A session’s token_id is a real trk_ id, not the raw RFID/app credential the charger presented as an OCPP idTag. That raw value lives only on the tokens resource (Token.uid) and is never copied onto a session, if you used to read the old id_tag field directly, token_id is what replaced it.

Note

Treat every id as an opaque string once you have it. The prefix is a stable signal you can branch on, the characters after it aren’t. Don’t decode them, don’t assume a fixed length, don’t sort by them for anything other than a display order you control yourself. Hyrum’s Law cuts both ways here: if enough integrations start depending on some incidental property of today’s ids (a length, a charset, an ordering), that incidental property becomes a contract we have to keep forever. We’d rather keep the actual contract narrow, the prefix is guaranteed, nothing else about the string is.

Errors

Every error response, from a malformed request to a database timeout, comes back as the same shape: RFC 7807 Problem Details, served with Content-Type: application/problem+json.

FieldTypeNotes
typestring (URI)A stable identifier for the error category, e.g. https://api.ampr.dev/errors/not-found. Safe to match on in code.
titlestringShort, human-readable summary. Wording can change without it being a breaking change.
statusintegerThe same value as the HTTP status code.
detailstring or nullHuman-readable specifics for this particular occurrence.
instancestring or nullThe request path that produced the error.

A request for a charger that doesn’t exist in your organization:

{
  "type": "https://api.ampr.dev/errors/not-found",
  "title": "Charger not found",
  "status": 404,
  "detail": "No charger with ID chg_bogus000 exists in your organization.",
  "instance": "/v1/chargers/chg_bogus000"
}
Handle a 404
curl -i https://api.ampr.dev/v1/chargers/chg_bogus000 \
-H "Authorization: Bearer $AMPR_API_KEY"
# HTTP/1.1 404 Not Found
# Content-Type: application/problem+json

Some validation failures carry one more field. POST /webhooks with no url and no events returns every field-level problem at once instead of making you fix and resubmit one at a time:

{
  "type": "https://api.ampr.dev/errors/validation",
  "title": "Validation failed",
  "status": 400,
  "detail": "url: is required; events: at least one event type is required",
  "instance": "/v1/webhooks",
  "errors": [
    { "field": "url", "message": "is required" },
    { "field": "events", "message": "at least one event type is required" }
  ]
}

The spec’s base error schema only requires type, title, and status, detail and instance are optional, and errors is additional detail included on field-level validation failures like this one.

The handle webhooks guide covers the related case, subscribing to an event type outside the documented set (say, charger.exploded) doesn’t fail the same way. It’s a well-formed request the API understands but refuses to act on, so it comes back as a 422 with a plain detail string instead of an errors array:

{
  "type": "https://api.ampr.dev/errors/unprocessable",
  "title": "Unprocessable entity",
  "status": 422,
  "detail": "Unknown event type(s): charger.exploded. Valid types: charger.connected, charger.disconnected, ...",
  "instance": "/v1/webhooks"
}

400 means the shape of the request is wrong, 422 means the shape is fine but the content can’t be carried out. Match on type, not status, if you need to tell cases like these apart programmatically, the status code alone collapses distinctions you might want to handle differently.

Versioning & stability

The version lives in the URL, not a header: every call in this API goes through https://api.ampr.dev/v1. There’s one version today. If a change ever breaks a documented response shape, it moves to the next path segment rather than mutating /v1 underneath you, so code written against /v1 keeps working for as long as /v1 is served.

That’s the same posture the API takes toward its own database. Migrations ship additive-only in the same deploy as the code that needs them, and a destructive change (dropping a column, tightening a type) splits across an expand step and a later contract step, so the schema and the running code are never more than one deploy apart. The public surface gets the same treatment: a session’s token_id field replacing the old raw id_tag field is the one breaking change of that shape so far, the new field shipped and consumers moved onto it before the old one was ever retired.

Three things are frozen contracts, held to that same standard:

  • The OpenAPI spec. Request and response shapes, required fields, and enum values don’t shift out from under a version. New optional fields can be added; existing ones aren’t repurposed or removed without a version change.
  • Webhook payloads. The envelope (id, type, created_at, data) and the data shape for each event type in the handle webhooks guide are as stable as any REST response, a delivery is a request you didn’t initiate, but it’s still /v1.
  • Id formats. The prefixes in the table above are permanent. New resources get new prefixes; existing ones never change shape.
Warning

Nothing outside the documented response shape is part of the contract, field ordering in the JSON, undocumented properties that happen to be present today, or timing between webhook deliveries beyond what handle webhooks specifies. If your integration depends on something this guide doesn’t describe, treat that as a bug in your integration, not a feature you can rely on staying put.