Skip to main content
The sandbox is a private copy of your fleet for building and testing. It has the same API, the same code paths and the same webhooks as your live fleet, but its drivers are fake, its chargers are simulated and its payments never move money. You reach it with a sandbox key (cxk_test_…). Everything a sandbox key does happens in the sandbox; everything a live key (cxk_live_…) does happens on your real fleet. The base URL is the same for both.

Test and live keys

You choose the environment for each key when you create it: in the fleet portal open Developers → API keys → Create key and pick Live or Sandbox. Sandbox and live are fully separate. Keys, webhook endpoints, events, drivers, vehicles, sessions and the wallet belong to one environment and are never visible from the other. A webhook endpoint registered with a sandbox key only receives sandbox events, and the portal’s Developers pages show one environment at a time. The sandbox is created the first time you use a sandbox key, so your first request may take a moment longer.

What’s in the sandbox

A new sandbox (and one you’ve just reset) starts with:
  • Three drivers, Ama Owusu, Kwame Asante and Efua Mensah (employee ids SBX-001 to SBX-003). They have placeholder emails ending in @sandbox.chargeexpress.invalid, no logins, and are never emailed.
  • A vehicle for each driver, registered SBX 1001-26, SBX 1002-26 and SBX 1003-26.
  • A wallet with 500 kWh of prepaid credit and a GHS 1,000 credit limit, at your live fleet’s unit price.
Drivers you create in the sandbox with POST /drivers also get a placeholder email, whatever address you send, and the response has login_invite.status: "skipped":
GET /fleet in the sandbox returns the sandbox copy of your fleet. Its id is different from your live fleet’s id, and it is the fleet_id on sandbox events.

Simulated chargers

With a sandbox key, GET /charge_points returns only these four charge points, each with simulated: true. Real chargers can’t be reached from the sandbox: sandbox sessions only run on these four.

How the simulation runs

Sandbox sessions run on a simulated meter at 60 times real time:
  • Energy delivered is max_power_kw × elapsed time × 60, capped by the session’s budget. One real second is one simulated minute, so SIM-DC-60 delivers 1 kWh per second and SIM-AC-22 about 0.37 kWh per second.
  • A server job advances every running sandbox session every 5 seconds, so meter readings and events move in 5-second steps.
  • A session ends on its own after about 2 minutes (two simulated hours), or as soon as it reaches its budget, whichever comes first.
  • When a session reaches its budget you get session.stopped_by_limit with stop_reason: "budget_reached", then session.completed.
  • Stopping a session in the portal ends it right away and settles the energy delivered so far.
The budget works as in live: the smallest of the session’s max kWh (set when you start it in the portal), any fleet or driver limits, and what the wallet can cover. Limits, wallet reservations and debits, credit and limit.reached events all behave as they do live. Sandbox session ids look like sim_17918822001234567; treat them as opaque, like every id.

Walkthrough

This walkthrough lists the simulated chargers, runs a session on SIM-DC-60 until it hits a 10 kWh cap, and tops up the wallet. It needs a sandbox key with fleet:read and wallet:write, and a webhook endpoint subscribed to session.* and wallet.* (register one with the sandbox key as in the Quickstart).
1

List the charge points

Response
The real response lists all four simulated charge points; this sample shows one.
2

Start a session with a 10 kWh cap

In the fleet portal, open Developers → Sandbox and choose Start session. Pick a driver and their vehicle, charge point SIM-DC-60, connector 1, and a max kWh of 10.The simulated charger starts at once, so session.started (data.object.status: "charging") is sent to your endpoint straight away. Note the session id from data.object.id, or find it with GET /sessions?status=charging.
3

Watch it hit the cap

At 1 kWh per second, the session reaches 10 kWh after about 10 seconds and is stopped at the next 5-second step. Your endpoint receives session.stopped_by_limit:
session.stopped_by_limit data.object
followed by session.completed with status: "completed" and the final energy_kwh and cost. The 10 kWh are debited from the sandbox wallet. You can also poll GET /sessions/{sessionId}.To end a session early instead, select it in Developers → Sandbox and choose Stop. In the sandbox it settles immediately, and session.completed follows.
4

Create a top-up

Response (201)
There is no checkout page: checkout_url is null and sandbox is true. The funding stays pending until you settle it.
5

Mark it paid

The response is the funding with status: "success". The wallet is credited (any credit used is repaid first, as live) and wallet.funded is sent. To simulate a failed payment, call /fail instead: the funding becomes failed and wallet.funding_failed is sent. Settling a funding that is no longer pending returns 409 funding_not_pending.
Paying usage invoices works the same way: POST /invoices/usage/pay with a sandbox key returns a pending funding with checkout_url: null and sandbox: true, which you settle with /sandbox/fundings/{fundingId}/succeed or /fail. Both settle endpoints need the wallet:write scope.

Test failure paths

Start sessions on the two misbehaving chargers from Developers → Sandbox to exercise your error handling:
  • SIM-OFFLINE is never connected, so no session starts and nothing is reserved.
  • SIM-REJECT rejects the start: the wallet reservation is released and session.start_failed carries the session with status: "cancelled", as when a real charger rejects a start.
Other failures work as live, against the sandbox’s own data: set a tight limit with PUT /limits to get limit.reached, or fail a top-up to get wallet.funding_failed.

Send test events of any type

POST /webhook_endpoints/{endpointId}/test sends one signed event to an endpoint straight away. Pass type to send any event type with a realistic sample payload; without it you get a ping:
Response
This is the easiest way to see events the sandbox doesn’t produce on its own, such as invoice.created or invitation.accepted. Test events aren’t stored, retried or listed in /events, and they work with live keys too.

Reset the sandbox

POST /sandbox/reset clears the sandbox’s sessions, top-ups, drivers, vehicles, limits and events, then seeds fresh drivers, vehicles and wallet credit. Your keys and webhook endpoints are kept, so your integration keeps working.
Response
Ids change after a reset, so list drivers and vehicles again.

In the portal

Developers → Sandbox in the fleet portal does all of this without code: see the sandbox’s drivers, vehicles, wallet and recent activity, start and stop sessions on the simulated chargers, simulate paid or failed top-ups, and reset the sandbox.

What the sandbox never touches

  • Billing: nothing in the sandbox is billed. Sandbox credit and debt are not real money.
  • Invoicing: sandbox sessions and top-ups are never invoiced, so invoice_number stays null and invoice.created isn’t sent for sandbox activity.
  • Payments: sandbox top-ups and invoice payments never reach ExpressPay.
  • Real chargers: sandbox sessions never send commands to a charger.
  • Real people: sandbox drivers have no logins and are never emailed.
The /sandbox/* endpoints only work with a sandbox key. With a live key they return 403 sandbox_only.

Going live

When your integration works in the sandbox, create a live key in the production fleet portal and swap it in. Then register your webhook endpoints again with the live key (sandbox endpoints don’t receive live events), and check livemode in your webhook handler if the same server receives both.