The Plug n Drive API
Read your sites, chargers, charging sessions and drivers from your own tools: CRM, accounting, spreadsheets or loyalty programmes. REST and JSON, with one key per integration.
Connect the API to the tools your team already uses
Pipe sessions and drivers into your CRM, marketing and accounting tools, and reward drivers through loyalty programmes. The API is read-only today. We build the connection with you.














CRM and sales: Salesforce, HubSpot, Microsoft Dynamics 365, Zoho CRM. Marketing: Mailchimp. Accounting: Xero, Sage. Rewards and loyalty: Pick n Pay Smart Shopper, Checkers Xtra Savings, Discovery Vitality, Clicks ClubCard, Woolworths WRewards, FNB eBucks.
All product names, logos and brands are the property of their respective owners. Plug n Drive is not affiliated with, endorsed or sponsored by any of them. They illustrate the kinds of systems a custom integration can target.
Quick start
You can make your first call in three minutes.
- Create a key. Sign in to the Plug n Drive console as a client administrator, open API & Keys and choose Create key. Give it the name of the tool that will use it. Copy the key straight away: it is shown only once.
- Call an endpoint. Send the key in the
Authorizationheader. - Read the result. Every list comes back as
{"data": [...], "next_cursor": ...}.
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://ozlnxhtguaoeuhjeejxq.supabase.co/functions/v1/api-v1/v1/sessions?from=2026-10-01&limit=50"const res = await fetch("https://ozlnxhtguaoeuhjeejxq.supabase.co/functions/v1/api-v1/v1/sessions?from=2026-10-01&limit=50", {
headers: { Authorization: `Bearer ${process.env.PND_API_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${(await res.json()).error.message}`);
const { data, next_cursor } = await res.json();
console.log(data.length, "sessions, next page:", next_cursor);import os, requests
BASE = "https://ozlnxhtguaoeuhjeejxq.supabase.co/functions/v1/api-v1"
headers = {"Authorization": f"Bearer {os.environ['PND_API_KEY']}"}
sessions, cursor = [], None
while True:
params = {"from": "2026-10-01", "limit": 200}
if cursor:
params["cursor"] = cursor
r = requests.get(f"{BASE}/v1/sessions", headers=headers, params=params, timeout=30)
r.raise_for_status()
body = r.json()
sessions += body["data"]
cursor = body["next_cursor"]
if not cursor:
break
print(len(sessions), "sessions")Keep your key on a server. Never put it in a web page, a mobile app or a public repository. If a key is exposed, revoke it in the console and create a new one. It takes effect immediately.
Base URL
https://ozlnxhtguaoeuhjeejxq.supabase.co/functions/v1/api-v1All endpoints below are relative to this address and start with /v1. Keep the base URL in one configuration setting in your code, so a future change of address is a one-line update.
Authentication
Send your API key as a bearer token:
Authorization: Bearer pnd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxYou can also send it in an X-API-Key header. Keys begin with pnd_live_.
- A key belongs to one client and only ever returns that client's data.
- Keys are read-only. Every request must be a
GET. - Create one key per tool (for example "CRM" and "Accounting"), so you can revoke one without affecting the other.
- You can have up to 10 active keys. The console shows when each key was last used.
A missing, malformed or revoked key returns 401:
{
"error": {
"code": "invalid_api_key",
"message": "Missing or invalid API key."
}
}Conventions
- Format. Responses are JSON. Timestamps are ISO 8601 in UTC. Amounts are decimal numbers in the currency named in the same record, such as
ZARorMZN. - Identifiers. Ids are UUID strings and stay stable. Use them to link records, for example
site_idon a charger ordriver_idon a session. - Dates.
fromandtoaccept2026-10-01or a full timestamp such as2026-10-01T00:00:00Z.fromis inclusive andtois exclusive. - Missing values are returned as
null.
Pagination
List endpoints return up to limit records (1 to 200, default 100). When more records exist, the response contains a next_cursor. Pass it back as cursor to get the next page. When next_cursor is null, you have everything.
GET /v1/sessions?from=2026-10-01&limit=200
GET /v1/sessions?from=2026-10-01&limit=200&cursor=2026-10-08T15:26:31.382851Z_9d3f5b1a-6c84-47e2-b0a9-2f7e1c4d8a63Treat the cursor as an opaque value: copy it as it is, and keep the same filters between pages. Sessions are returned oldest first, so you can store the last cursor and continue from it later.
Sites
/v1/sitesYour sites, including sub-sites such as buildings or parking areas (parent_id links a sub-site to its parent).
Parameters
| Name | Type | Description |
|---|---|---|
limit | integer | 1 to 200. Default 100. |
cursor | string | From a previous next_cursor. |
Fields
| Field | Type | Description |
|---|---|---|
id | uuid | Site id. |
name | string | Site name. |
parent_id | uuid | null | Parent site, or null for a top-level site. |
type | string | For example Building or Parking. |
city | string | null | |
address | string | null | |
currency | string | Currency used for this site's tariffs and sessions. |
timezone | string | IANA time zone, for example Africa/Johannesburg. |
status | string | Active, Pending install or Inactive. |
latitude / longitude | number | null | Decimal degrees. |
capacity_kva | number | null | Grid connection capacity. |
Example
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://ozlnxhtguaoeuhjeejxq.supabase.co/functions/v1/api-v1/v1/sites"{
"data": [
{
"id": "6f1c2a9e-3b7d-4d52-9a0e-1c8b5e7f2a41",
"name": "Example Complex - Block A",
"parent_id": null,
"type": "Building",
"city": "Sandton",
"address": "1 Example Road, Sandton, 2196",
"currency": "ZAR",
"timezone": "Africa/Johannesburg",
"status": "Active",
"latitude": -26.107,
"longitude": 28.056,
"capacity_kva": 400
}
],
"next_cursor": null
}Chargers
/v1/chargersYour chargers and their current status.
Parameters
| Name | Type | Description |
|---|---|---|
site_id | uuid | Only chargers of this site. |
limit | integer | 1 to 200. Default 100. |
cursor | string | From a previous next_cursor. |
Fields
| Field | Type | Description |
|---|---|---|
id | uuid | Charger id. |
name | string | |
site_id | uuid | The site this charger belongs to. |
power_kw | number | Rated power in kW. |
connector | string | null | For example Type 2 or CCS2. |
status | string | For example Available, Charging or Offline. |
last_seen_at | timestamp | null | Last time the charger contacted the platform. |
listed_publicly | boolean | Whether the charger appears on the public map. |
Example
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://ozlnxhtguaoeuhjeejxq.supabase.co/functions/v1/api-v1/v1/chargers?site_id=6f1c2a9e-3b7d-4d52-9a0e-1c8b5e7f2a41"{
"data": [
{
"id": "b2e8d4f6-71a3-4c95-8e10-5d9a3c7b1f20",
"name": "A1 Fast 180 kW",
"site_id": "6f1c2a9e-3b7d-4d52-9a0e-1c8b5e7f2a41",
"power_kw": 180,
"connector": "CCS2",
"status": "Available",
"last_seen_at": "2026-10-09T08:41:12.512+00:00",
"listed_publicly": true
}
],
"next_cursor": null
}Sessions
/v1/sessionsCharging sessions on your chargers, oldest first. Use it to feed accounting tools, reports or dashboards.
Parameters
| Name | Type | Description |
|---|---|---|
from | date | Sessions that started on or after this moment. |
to | date | Sessions that started before this moment. |
site_id | uuid | Only sessions at this site. |
limit | integer | 1 to 200. Default 100. |
cursor | string | From a previous next_cursor. |
Fields
| Field | Type | Description |
|---|---|---|
id | uuid | Session id. |
started_at / ended_at | timestamp | Start and end of the session. |
duration_seconds | number | Length of the session. |
kwh | number | Energy delivered. |
gross_amount | number | Amount charged for the session. |
net_amount | number | Your share after the Plug n Drive commission, as shown in the Reporting page of the console. |
currency | string | Currency of both amounts. |
payment_method | string | How the session was paid, for example RFID card. |
charger_id / charger_name | uuid / string | |
site_id / site_name | uuid / string | |
driver_id | uuid | null | Links to id in /v1/drivers. |
Example
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://ozlnxhtguaoeuhjeejxq.supabase.co/functions/v1/api-v1/v1/sessions?from=2026-10-01&limit=1"{
"data": [
{
"id": "9d3f5b1a-6c84-47e2-b0a9-2f7e1c4d8a63",
"started_at": "2026-10-08T15:26:31.382851+00:00",
"ended_at": "2026-10-08T17:39:09.277588+00:00",
"duration_seconds": 7957.89,
"kwh": 21,
"gross_amount": 100.8,
"net_amount": 93.74,
"currency": "ZAR",
"payment_method": "RFID card",
"charger_id": "b2e8d4f6-71a3-4c95-8e10-5d9a3c7b1f20",
"charger_name": "A1 Fast 180 kW",
"site_id": "6f1c2a9e-3b7d-4d52-9a0e-1c8b5e7f2a41",
"site_name": "Example Complex - Block A",
"driver_id": "e4a7c1d9-52b8-4f36-a1d0-8c3e6b9f7a25"
}
],
"next_cursor": "2026-10-08T15:26:31.382851Z_9d3f5b1a-6c84-47e2-b0a9-2f7e1c4d8a63"
}Drivers
/v1/driversPeople who completed at least one session on your chargers, with totals over the period. Contact details are included only when the driver agreed to share them with you. See Personal data.
Parameters
| Name | Type | Description |
|---|---|---|
from | date | Count sessions that started on or after this moment. |
to | date | Count sessions that started before this moment. |
limit | integer | 1 to 200. Default 100. |
cursor | string | From a previous next_cursor. |
Fields
| Field | Type | Description |
|---|---|---|
id | uuid | Driver id, the same value as driver_id on sessions. |
name | string | |
type | string | app_customer for drivers who use the Plug n Drive app or website, cardholder for people with an RFID card issued for your site. |
sessions | integer | Number of completed sessions in the period. |
kwh_total | number | Energy delivered in the period. |
spent | object | Amount charged in the period, per currency, for example {"ZAR": 885.6}. |
first_session_at / last_session_at | timestamp | First and last session in the period. |
marketing_consent | boolean | null | true or false for app customers, according to their choice for your organisation. null for cardholders. |
email / phone | string | null | Present only when allowed (see below), otherwise null. |
Example
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://ozlnxhtguaoeuhjeejxq.supabase.co/functions/v1/api-v1/v1/drivers?from=2026-08-01"{
"data": [
{
"id": "e4a7c1d9-52b8-4f36-a1d0-8c3e6b9f7a25",
"name": "Jane Example",
"type": "app_customer",
"sessions": 12,
"kwh_total": 184.5,
"spent": {
"ZAR": 885.6
},
"first_session_at": "2026-08-12T17:02:44+00:00",
"last_session_at": "2026-10-08T15:26:31+00:00",
"marketing_consent": true,
"email": "jane@example.com",
"phone": "+27 82 000 0000"
},
{
"id": "a1b2c3d4-0000-4000-8000-0123456789ab",
"name": "John Example",
"type": "app_customer",
"sessions": 3,
"kwh_total": 41.2,
"spent": {
"ZAR": 197.8
},
"first_session_at": "2026-09-30T18:10:00+00:00",
"last_session_at": "2026-10-05T18:40:00+00:00",
"marketing_consent": false,
"email": null,
"phone": null
}
],
"next_cursor": null
}Personal data
Drivers' details belong to the drivers, and your use of them is covered by data protection law, including POPIA in South Africa. The API is designed around that.
- Contact details are gated. For app customers,
emailandphoneare returned only if the driver agreed to receive offers and news from your organisation. Otherwise they arenull. - Cardholders issued by you, such as residents or staff with an RFID card, are your own contacts, so their details are returned.
- Consent is per organisation. A driver who accepted offers from one site operator has not accepted them from another.
- You must honour changes. Drivers can switch their choice off at any time. Re-read
marketing_consentbefore each campaign instead of keeping an old copy. - Never returned: wallet balances, saved payment cards and payment details.
This page describes how the API behaves. It is not legal advice. Your own privacy notice and your agreement with Plug n Drive set out what you may do with the data.
Errors
Errors use the usual HTTP status codes and a JSON body with a stable code and a readable message:
{
"error": {
"code": "invalid_api_key",
"message": "Missing or invalid API key."
}
}| Status | Code | Meaning |
|---|---|---|
400 | invalid_parameter | A parameter is malformed, for example limit=500 or a date that does not exist. The message says which one. |
401 | invalid_api_key | The key is missing, malformed, unknown or revoked. |
404 | not_found | Unknown endpoint. |
405 | method_not_allowed | Only GET is supported. |
429 | rate_limited | Too many requests. Wait for the number of seconds in the Retry-After header. |
500 | server_error | Something went wrong on our side. Try again in a moment, and contact us if it persists. |
Limits
| Limit | Value |
|---|---|
| Requests | 60 per minute per key |
| Page size | 200 records at most |
| Active keys | 10 per organisation |
| Methods | GET only |
For regular synchronisation, fetch with limit=200, store the last next_cursor, and ask only for what is new with from. A schedule of every few minutes is far below the limit.
Changelog
v1, 9 October 2026
- First release:
/v1/sites,/v1/chargers,/v1/sessionsand/v1/drivers. - Read-only access with one key per integration, managed in the console under API & Keys.
We add fields without notice, so ignore fields you do not know. Changes that could break an integration will arrive in a new version, with advance notice.
Need something else? Webhooks, write access and other endpoints are on our list. Tell us what you would build at hello@plugndrive.co.za.