API Reference · Version 1.3.16
IMBox API
A data-acquisition agnostic API for the IMBox smart locker platform. Send identifiers, receive access codes — no personal data crosses the boundary in either direction.
1 Introduction
The IMBox API is a data-acquisition agnostic API for the IMBox smart locker platform. It lets your system create and track parcel orders, monitor locker stations, and receive events — without ever handling customer or courier personal data.
Two properties define it.
Data-acquisition agnostic. The API operates entirely
on identifiers you choose. A customer is CUST-9917 because
you say so. Requests carrying such fields are rejected outright, so the
guarantee is enforced by the software rather than promised in a
contract. The correspondence between an identifier and a real person
stays inside your systems, where it already is.
Stable and extensible. The API presents a small
contract that does not move. New station types, new cell sizes and new
capabilities are introduced additively — existing calls keep working,
fields are added and never repurposed, and the API is versioned under
/api/v1/ so a breaking change would arrive as
/api/v2/ alongside it.
1.1 What the API does for you
| You send | The API does |
|---|---|
customer_id |
Finds or creates the matching record |
courier_id |
Finds or creates the matching record |
billing_id |
Finds or creates the pricing plan |
notification_id |
Finds or creates the message template |
station_id,
cell_size |
Reserves a physical cell and returns its access codes |
Every one of those lookups is cached after first use, is safe to repeat, and never requires you to know an internal primary key.
1.2 Live deployment
The API is deployed and serving.
| Base URL | https://ecommerce.api.imbox.solutions |
| API root | https://ecommerce.api.imbox.solutions/api/v1/ |
| Console | https://ecommerce.api.imbox.solutions/console/ |
| Health | https://ecommerce.api.imbox.solutions/api/v1/health/ |
2 The console
The console is a browser interface for your developers. It is where API credentials are obtained and where the webhook endpoints are configured. It requires no separate account: you sign in with the same organisation credentials you already hold with IMBox.
2.1 Signing in
| Field | Description |
|---|---|
| Username | Your IMBox organisation name, for example
Vodafone. |
| Password | The password for that organisation account. |
The API holds no password of its own. Each sign-in is verified against your IMBox account and accepted only if it is valid and the account is an organisation. Two consequences follow: there is no second password for your team to manage, and revoking your IMBox account revokes console access at the same moment.
Sign-in attempts are rate limited to five per minute per source address.
2.2 Overview
The landing page after sign-in contains information about the organisation’s activity and webhook configuration.
| Element | Description |
|---|---|
| Orders created | Total orders this organisation has created through the API. |
| Currently open | Orders not yet collected — status
CREATED or DROPPED or etc. |
| Webhooks configured | How many of the six events have a live endpoint. |
| Organisation id | Your IMBox organisation identifier. |
| Webhook endpoints | Each event, its destination URL, and whether it is active. |
| Recent deliveries | The ten most recent callback attempts with their outcome. |
2.3 Credentials
Where your developers collect the values their code needs. The page is divided into three independent credentials, each serving a different purpose.
2.3.1 OAuth2
Identifies your system when it calls the API.
| Field | Description |
|---|---|
| Token URL | Where to exchange your credentials for an access token. |
| Grant type | Always client_credentials.
There is no user-facing login flow. |
| Client id | Public identifier for your organisation. Always visible here. |
| Client secret | Stored as a one-way hash. Rotate to reveal a new value. |
2.3.2 Request signing (HMAC)
An optional second layer. OAuth2 alone is accepted by default; enabling signing additionally protects each request against tampering and replay.
| Field | Description |
|---|---|
| Key id | Public identifier of the signing key, sent
as X-IMBox-Key. |
| Secret | Encrypted at rest. Rotate to reveal a new value. |
2.3.3 Webhook signing secret
Used only in the outbound direction. It is never sent on an API request; it exists so that your endpoint can verify that a callback genuinely originated from the API.
| Field | Description |
|---|---|
| Value | Encrypted at rest. Rotate to reveal a new value. |
| Used for | Outbound webhooks only. |
| Algorithm | HMAC-SHA256. |
How the signature is constructed and verified is covered in Verifying a callback.
2.3.4 Obtaining a secret
Secrets are displayed exactly once, at the moment they are generated. Nothing in the system can display them again — they are stored hashed or encrypted.
To obtain one:
- Open Credentials.
- Press Rotate on the credential you need.
- Enter your organisation password when prompted, then confirm.
- Copy the revealed value immediately and store it in your secret manager.
Rotation is deliberately gated behind the password, not merely the session, because it invalidates a credential that may be carrying live traffic. An unattended browser is not sufficient authority to break a running integration.
The previous value stops working the instant a rotation completes. Deploy the new secret at the same time.
2.4 Webhooks
One destination URL per event. The API posts to these as orders progress.
| Field | Description |
|---|---|
| Event name | The identifier sent in the
X-IMBox-Event header. |
| URL | Your HTTPS endpoint. |
| Enabled | Uncheck to suspend delivery without discarding the URL. |
| Last delivery | Time and status of the most recent attempt, plus any run of consecutive failures. |
| Send test | Posts a representative payload immediately and reports the status code, latency and response body. |
Send test is the fastest route to a working integration: it exercises your endpoint, your signature verification and your parser before a real parcel is involved.
Press Save all endpoints to persist changes.
2.5 Deliveries
An audit trail of the last fifty callback attempts.
| Column | Description |
|---|---|
| When | Time of the attempt. |
| Event | Which event was being delivered. |
| Order | The order the event concerns. |
| Status | delivered,
pending (awaiting retry), failed (retries
exhausted), or no endpoint. |
| Code | status code returned by your endpoint. |
| Tries | Attempts made so far. |
| Error | Transport error or response body from the failure. |
A status of no endpoint means the event occurred and was
recorded but no URL was configured for it. The event is still readable
through the activity log API, so nothing is lost.
3 Getting started
This walkthrough takes you from an empty console to a live order with
pickup codes and a working webhook, using nothing but curl.
Each step builds on the last; run them in order and you will have
exercised every part of the integration in a few minutes.
3.1 Collect your credentials
Sign in to the console and open Credentials (see The console section). You need two values:
- Client id — always shown on the page.
- Client secret — press Rotate to reveal it once, then copy it straight into your secret manager.
Export them so the commands below can reuse them:
export BASE="https://ecommerce.api.imbox.solutions"
export CLIENT_ID="<your client id>"
export CLIENT_SECRET="<your client secret>"3.2 Get an access token
export TOKEN=$(curl -s -X POST "$BASE/oauth2/token/" \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials \
| python -c "import sys, json; print(json.load(sys.stdin)['access_token'])")
echo "$TOKEN"The token lasts an hour — cache it and reuse it on every call below.
3.3 Create a test order
Send identifiers only. The API resolves the customer and courier, reserves a cell of the requested size, and returns the two access codes.
Note —
station_idmust be a locker station IMBox has assigned to your organisation. If you do not have one yet, please contact the IMBox team to have a station provisioned and to receive its identifier. The example below usesMALL-MASR-001for illustration.
curl -s -X POST "$BASE/api/v1/orders/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"station_id": "MALL-MASR-001",
"customer_id": "CUST-1001",
"courier_id": "CR-7",
"cell_size": "medium",
"order_id": "TEST-0001",
"idempotency_key": "TEST-0001"
}'Response — 201 Created
{
"order_id": "TEST-0001",
"status": "CREATED",
"station_id": "MALL-MASR-001",
"cell_id": "3",
"cell_size": "medium",
"customer_id": "CUST-1001",
"courier_id": "CR-7",
"customer_code": "8251372",
"courier_code": "2875912",
"codes_available": true
}The two codes are the heart of the flow:
courier_code opens the cell for the
courier to deposit the parcel, and
customer_code opens it for the recipient
to collect it. Distribute them through your own channels.
idempotency_key makes the call safe to retry — replaying
the same key returns the original order instead of creating a second
one.
3.4 List your orders and their status
curl -s "$BASE/api/v1/orders/?customer_id=CUST-1001" \
-H "Authorization: Bearer $TOKEN"Response — 200 OK
{
"count": 1,
"pages": 1,
"results": [
{ "order_id": "TEST-0001", "status": "CREATED", "cell_id": "3", "…": "…" }
]
}status moves through
CREATED → DROPPED → PICKED (or EXPIRED /
FAILED) as the courier and recipient act at the locker
keypad. Combine status=, date=,
station_id= and the other filters — see List orders.
3.5 Read one order’s codes and history
curl -s "$BASE/api/v1/orders/TEST-0001/" \
-H "Authorization: Bearer $TOKEN"This returns the full order — including customer_code
and courier_code — with its event history
(create, reserve, sync). If the
codes could not be read at creation time
(codes_available: false), retrieving the order fetches them
again.
3.6 Register a webhook and test it
Rather than polling, let the API call you as the order progresses.
- In the console, open Webhooks (see The console).
- Enter your HTTPS endpoint for the events you care about —
order.created,order.dropped,customer.notified,order.picked,order.expiredandorder.failed. - Press Send test. The API immediately POSTs a representative payload and reports the status code, latency and response body — so you validate your endpoint, your signature check and your parser before a real parcel is involved.
Every callback is signed. Verify it against the raw body, before any JSON parsing:
import hmac, hashlib, time
def verify(secret, headers, raw_body):
timestamp = headers.get("X-IMBox-Timestamp", "")
signature = headers.get("X-IMBox-Signature", "")
if not timestamp or not signature:
return False
if abs(time.time() - int(timestamp)) > 300: # reject stale replays
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)The webhook signing secret is on the Credentials page; the full payload and header reference is in Webhooks.
3.7 Force a status refresh (optional)
The API keeps statuses current on its own. To pull the latest immediately — handy while testing — call:
curl -s -X POST "$BASE/api/v1/orders/sync/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d '{}'Any status change it discovers dispatches the matching webhook. You have now created an order, read its codes, listed it, and wired up callbacks — the whole integration in miniature.
4 Authentication
Every API call carries an OAuth2 bearer token. Obtaining one is a single request.
4.1 Requesting a token
POST /oauth2/token/
Send your client id and secret using Basic authentication.
curl -X POST https://ecommerce.api.imbox.solutions/oauth2/token/ \
-u "<client_id>:<client_secret>" \
-d grant_type=client_credentialsResponse — 200 OK
{
"access_token": "Q5nHSrdYTtIc9m2K0pLxWvB3aRt7Ye",
"expires_in": 3600,
"token_type": "Bearer",
"scope": "orders:read orders:write stations:read logs:read"
}| Field | Description |
|---|---|
access_token |
Send as
Authorization: Bearer <token> on every API call. |
expires_in |
Lifetime in seconds. Cache the token and reuse it. |
scope |
Permissions granted to this token. |
Response — 401 Unauthorized — the
client id or secret is wrong.
4.2 Scopes
| Scope | Grants |
|---|---|
orders:read |
Reading orders, customers and couriers |
orders:write |
Creating orders and forcing a status refresh |
stations:read |
Reading stations and cells |
logs:read |
Reading the activity log |
4.3 Optional request signing
Where payload integrity and replay protection are required, the API
also accepts IMBOX-HMAC-SHA256 request signatures alongside
the bearer token. Four additional headers are sent:
X-IMBox-Key, X-IMBox-Timestamp,
X-IMBox-Nonce and X-IMBox-Signature. The
signature covers the method, path, canonical query string, body digest,
timestamp, nonce and OAuth2 client id, so a captured signature cannot be
replayed, redirected to another endpoint, or used by another
organisation. Contact the IMBox team to enable enforcement for your
organisation.
5 API reference
Base URL:
https://ecommerce.api.imbox.solutions/api/v1/
All requests require
Authorization: Bearer <token>. All request bodies are
JSON with Content-Type: application/json.
5.1 Create an order
POST /api/v1/orders/ — scope
orders:write
Creates a parcel order from identifiers alone. The API resolves the customer, courier, billing plan and notification template — creating any that do not yet exist — reserves a cell of the requested size at the station, and returns the access codes for the courier and the customer.
5.1.1 Request parameters
Required
| Parameter | Type | Description |
|---|---|---|
station_id |
string | Locker station identifier,
e.g. MALL-MASR-001. |
customer_id |
string | Your reference for the recipient. Created on first use. |
courier_id |
string | Your reference for the delivering courier. Created on first use. |
Note —
station_idmust identify a locker station IMBox has assigned to your organisation. Please contact the IMBox team to buy an assigned station and to obtain its identifier before creating orders.
Optional
| Parameter | Type | Default | Description |
|---|---|---|---|
cell_size |
string | medium |
One of small,
medium, large, xlarge. |
billing_id |
string | STANDARD |
Pricing plan reference. Created on first use. |
notification_id |
string | DEFAULT |
Message template reference. Created on first use. |
order_id |
string | generated | Your order reference. Must be unique for your organisation. |
dropped_date |
date | today | YYYY-MM-DD. When the courier
will deposit the parcel. |
picked_date |
date | +3 days | YYYY-MM-DD. End of the
collection window. |
idempotency_key |
string | — | Replaying a key returns the original order instead of creating a second. |
Identifier fields accept up to 128 characters from
A–Z a–z 0–9 . _ : @ -.
5.1.2 Example
curl -X POST https://ecommerce.api.imbox.solutions/api/v1/orders/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"station_id": "MALL-MASR-001",
"customer_id": "CUST-9917",
"courier_id": "CR-42",
"cell_size": "small",
"order_id": "VF-2026-0001",
"idempotency_key": "VF-2026-0001"
}'5.1.3
Response — 201 Created
{
"order_id": "VF-2026-0001",
"status": "CREATED",
"station_id": "MALL-MASR-001",
"cell_id": "2",
"cell_size": "small",
"customer_id": "CUST-9917",
"courier_id": "CR-42",
"billing_id": "STANDARD",
"notification_id": "DEFAULT",
"customer_code": "8251372",
"courier_code": "2875912",
"codes_available": true,
"dropped_date": "2026-08-26",
"picked_date": "2026-08-29",
"upstream_order_id": "28",
"failure_reason": "",
"created_at": "2026-08-26T09:14:02.113Z",
"dropped_at": null,
"picked_at": null
}| Field | Description |
|---|---|
order_id |
Your reference, or one generated for you. |
status |
CREATED,
DROPPED, PICKED, EXPIRED,
CANCELLED or FAILED. |
cell_id |
The physical cell reserved at the station. |
customer_code |
Keypad code for the recipient to collect the parcel. |
courier_code |
Keypad code for the courier to deposit it. |
codes_available |
false indicates the codes
could not be read, not that none exist. |
upstream_order_id |
The order’s internal reference, for support. |
failure_reason |
Populated only when status is
FAILED. |
Distribute the two codes through your own channels. The API has no address to send them to, which is precisely the point.
5.1.4 Other responses
| Status | error |
Meaning |
|---|---|---|
400 |
validation_error |
A field is missing, malformed, or not part
of the contract. See fields. |
401 |
— | Token missing, malformed or expired. |
403 |
insufficient_scope |
The token lacks
orders:write. |
409 |
conflict |
An order with that order_id
already exists. |
409 |
no_capacity |
No free cell of the requested size at that station. |
422 |
provisioning_failed |
A referenced record could not be created. |
429 |
— | Rate limit exceeded. Back off and retry. |
502 / 503 |
upstream_unavailable |
The service is temporarily unavailable. |
A rejected personal-data field looks like this:
{
"error": "validation_error",
"detail": "One or more fields are invalid.",
"request_id": "c014d4147a2c47fb88e3228724a99027",
"fields": {
"customer_name": ["Unknown field. Send identifiers only - no personal data."],
"customer_phone": ["Unknown field. Send identifiers only - no personal data."]
}
}5.2 List orders
GET /api/v1/orders/ — scope
orders:read
Returns your organisation’s orders, newest first. Every parameter is optional and they combine.
| Parameter | Type | Description |
|---|---|---|
order_id |
string | Exact match on your order reference. |
customer_id |
string | All orders for one recipient. |
courier_id |
string | All orders for one courier. |
station_id |
string | All orders at one locker station. |
cell_id |
string | All orders that used one cell. |
organization_id |
string | Your organisation. Other values return an empty set. |
status |
string | CREATED,
DROPPED, PICKED, EXPIRED,
CANCELLED, FAILED. |
date |
string | today,
yesterday, last_week, last_month,
last_year. |
page |
integer | Page number, from 1. |
page_size |
integer | Results per page. Default 50, maximum 200. |
curl "https://ecommerce.api.imbox.solutions/api/v1/orders/?customer_id=CUST-9917&status=CREATED" \
-H "Authorization: Bearer $TOKEN"5.2.1
Response — 200 OK
{
"count": 12,
"pages": 1,
"page": 1,
"page_size": 50,
"next": null,
"previous": null,
"results": [ { "order_id": "VF-2026-0001", "status": "CREATED", "…": "…" } ]
}| Field | Description |
|---|---|
count |
Total matching orders across all pages. |
pages |
Number of pages available. |
next /
previous |
Absolute URLs, or null at the
ends. |
results |
Array of order objects, as returned by create. |
Other responses: 401,
403 insufficient_scope.
5.3 Retrieve one order
GET /api/v1/orders/{order_id}/ — scope
orders:read
Returns a single order together with its full history.
curl https://ecommerce.api.imbox.solutions/api/v1/orders/VF-2026-0001/ \
-H "Authorization: Bearer $TOKEN"5.3.1
Response — 200 OK
The order object, plus:
{
"order_id": "VF-2026-0001",
"status": "DROPPED",
"events": [
{ "action": "create", "to_status": "CREATED", "note": "Order accepted",
"occurred_at": "2026-08-26T09:14:02Z" },
{ "action": "reserve", "to_status": "CREATED", "note": "Cell 2 reserved",
"occurred_at": "2026-08-26T09:14:07Z" },
{ "action": "sync", "from_status": "CREATED", "to_status": "DROPPED",
"note": "Status changed at the locker",
"occurred_at": "2026-08-26T11:20:14Z" }
]
}| Field | Description |
|---|---|
events[].action |
create, reserve,
sync or fail. |
events[].from_status /
to_status |
The transition recorded. |
events[].occurred_at |
When it happened, UTC. |
Other responses: 404 not_found when no such order exists
for your organisation.
5.4 Refresh order statuses
POST /api/v1/orders/sync/ — scope
orders:write
The courier and the recipient act at the locker keypad, not through this API, so the API discovers those transitions on its own. This runs automatically every two minutes. Call this endpoint to force an immediate refresh.
curl -X POST https://ecommerce.api.imbox.solutions/api/v1/orders/sync/ \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}'5.4.1
Response — 200 OK
{ "synced": 3 }synced is the number of orders whose status changed. Any
resulting webhooks are dispatched as part of the call.
5.5 Activity log
GET /api/v1/logs/ — scope
logs:read
An append-only record of everything that has happened to your orders and stations, filterable on every identifier you already hold.
| Parameter | Type | Description |
|---|---|---|
order_id |
string | Everything concerning one order. |
customer_id |
string | Everything concerning one recipient. |
courier_id |
string | Everything concerning one courier. |
station_id |
string | Everything at one locker station. |
cell_id |
string | Everything involving one cell. |
organization_id |
string | Your organisation. |
event_type |
string | See the table below. Repeatable. |
date |
string | today,
yesterday, last_week, last_month,
last_year. |
since /
until |
ISO 8601 | Explicit time bounds. |
page /
page_size |
integer | Pagination. |
event_type |
Recorded when |
|---|---|
ORDER_CREATED |
An order was accepted and a cell reserved. |
ORDER_DROPPED |
The courier deposited the parcel. |
ORDER_PICKED |
The recipient collected the parcel. |
ORDER_EXPIRED |
The collection window elapsed. |
ORDER_FAILED |
The order could not be completed. |
CUSTOMER_NOTIFIED |
The recipient’s collection code was issued. |
RESOURCE_PROVISIONED |
An identifier was registered. |
WEBHOOK_SENT |
A callback was delivered successfully. |
WEBHOOK_FAILED |
A callback attempt failed. |
UPSTREAM_ERROR |
The service returned an error. |
curl "https://ecommerce.api.imbox.solutions/api/v1/logs/?station_id=MALL-MASR-001&date=last_week" \
-H "Authorization: Bearer $TOKEN"5.5.1
Response — 200 OK
{
"count": 3,
"results": [
{
"occurred_at": "2026-08-26T09:14:07Z",
"event_type": "ORDER_CREATED",
"station_id": "MALL-MASR-001",
"cell_id": "2",
"order_id": "VF-2026-0001",
"customer_id": "CUST-9917",
"courier_id": "CR-42",
"organization_id": "12",
"message": "Order VF-2026-0001 is CREATED",
"request_id": "c014d4147a2c47fb88e3228724a99027"
}
]
}request_id correlates the entry with the API’s own logs;
quote it in any support request.
5.6 List locker stations
GET /api/v1/stations/ — scope
stations:read
curl https://ecommerce.api.imbox.solutions/api/v1/stations/ \
-H "Authorization: Bearer $TOKEN"5.6.1
Response — 200 OK
[
{
"station_id": "MALL-MASR-001",
"name": "Mall Masr",
"location": "6th of October",
"status": "ACTIVE",
"cells_total": 6,
"cells_free": 2,
"cells_occupied": 1,
"cells_reserved": 3
}
]| Field | Description |
|---|---|
station_id |
Use this as station_id when
creating an order. |
status |
ACTIVE or
INACTIVE. |
cells_total |
Cells installed at the station. |
cells_free |
Cells available for a new order right now. |
cells_occupied |
Cells holding a parcel awaiting collection. |
cells_reserved |
Cells allocated to an order not yet deposited. |
5.7 Retrieve one station
GET /api/v1/stations/{station_id}/ —
scope stations:read
Returns the single station object shown above.
Other responses: 404 not_found.
5.8 List cells at a station
GET /api/v1/stations/{station_id}/cells/
— scope stations:read
| Parameter | Type | Description |
|---|---|---|
status |
string | free, occupied
or reserved. |
size |
string | small, medium,
large or xlarge. |
curl "https://ecommerce.api.imbox.solutions/api/v1/stations/MALL-MASR-001/cells/?status=free" \
-H "Authorization: Bearer $TOKEN"5.8.1
Response — 200 OK
[
{
"cell_id": "2",
"station_id": "MALL-MASR-001",
"station_name": "Mall Masr",
"size": "small",
"status": "free",
"state": "active"
}
]| Field | Description |
|---|---|
cell_id |
Identifier of the physical compartment. |
size |
Cell size class. |
status |
Current occupancy. |
state |
active, or
not_active when withdrawn from service. |
Checking capacity before creating an order is optional — the API
selects a cell for you and returns 409 no_capacity if none
is available.
5.9 Customers and couriers
GET /api/v1/customers/,
GET /api/v1/couriers/ — scope
orders:read
The identifiers your organisation has used, with per-party counters. No personal attribute is returned, because none is held.
| Parameter | Type | Description |
|---|---|---|
id |
string | Substring match on the identifier. |
page /
page_size |
integer | Pagination. |
GET /api/v1/customers/{customer_id}/,
GET /api/v1/couriers/{courier_id}/ return
one record.
5.9.1
Response — 200 OK
{
"id": "CUST-9917",
"kind": "customer",
"provisioned": true,
"first_seen": "2026-08-01T09:14:22Z",
"last_seen": "2026-08-26T09:14:02Z",
"orders_total": 14,
"orders_open": 1,
"orders_picked": 12
}| Field | Description |
|---|---|
id |
The identifier you supplied. |
provisioned |
Whether a corresponding record exists. |
first_seen /
last_seen |
When the API first and last saw this identifier. |
orders_total |
Orders ever created for it. |
orders_open |
Orders currently CREATED or
DROPPED. |
orders_picked |
Orders collected. |
Other responses: 404 not_found for an identifier never
used.
5.10 Health
GET /api/v1/health/ — no authentication
required
curl https://ecommerce.api.imbox.solutions/api/v1/health/5.10.1 Response —
200 OK
{
"status": "ok",
"version": "1.3.16",
"database": "ok",
"upstream": {
"reachable": true,
"authenticated": true,
"configured": true
},
"codes_readable": true,
"hmac_required": false
}| Field | Description |
|---|---|
status |
ok, or degraded
when the API cannot serve normally. |
upstream.reachable |
Whether the locker service is responding. |
upstream.authenticated |
Whether the API can authenticate to it. |
upstream.configured |
Whether service credentials are present. |
hmac_required |
Whether request signing is enforced for this deployment. |
A 503 indicates the API itself is degraded. Suitable for
a load balancer or an uptime monitor.
6 Webhooks
Rather than polling, register an endpoint per event and let the API call you.
6.1 Events
| Event | Fires when |
|---|---|
order.created |
The order was accepted, a cell reserved and the courier code issued. |
order.dropped |
The courier opened the cell and deposited the parcel. |
customer.notified |
The recipient’s collection code was issued. |
order.picked |
The recipient opened the cell and collected the parcel. |
order.expired |
The collection window elapsed without collection. |
order.failed |
The order could not be completed. |
6.2 Payload
Every event uses the same envelope.
{
"event": "order.dropped",
"event_id": "evt_9c1e77a30b5d4f62",
"occurred_at": "2026-08-26T11:20:14Z",
"data": {
"order_id": "VF-2026-0001",
"status": "DROPPED",
"station_id": "MALL-MASR-001",
"cell_id": "2",
"cell_size": "small",
"customer_id": "CUST-9917",
"courier_id": "CR-42",
"billing_id": "STANDARD",
"notification_id": "DEFAULT",
"customer_code": "8251372",
"courier_code": "2875912"
}
}Identifiers and codes only — the same rule applies outbound as inbound.
6.3 Headers
| Header | Description |
|---|---|
X-IMBox-Event |
The event name. |
X-IMBox-Event-Id |
Unique delivery identifier, useful for deduplication. |
X-IMBox-Timestamp |
Unix epoch seconds, included in the signature. |
X-IMBox-Signature |
sha256=<hex digest>. |
6.4 Verifying a callback
import hmac, hashlib, time
def verify(secret, headers, raw_body):
timestamp = headers.get("X-IMBox-Timestamp", "")
signature = headers.get("X-IMBox-Signature", "")
if not timestamp or not signature:
return False
if abs(time.time() - int(timestamp)) > 300: # reject stale replays
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)Verify against the raw body, before any JSON parsing — re-serialising changes the bytes and the signature will not match.
6.5 Delivery behaviour
| Property | Value |
|---|---|
| Method | POST,
Content-Type: application/json |
| Timeout | 10 seconds |
| Success | Any 2xx |
| Retries | Up to 5 attempts, exponential backoff (2s, 4s, 8s, 16s, 32s) |
| Ordering | Not guaranteed — use
occurred_at |
| Duplicates | Possible — deduplicate on
event_id |
Delivery never blocks the API call that triggered it. Every attempt is visible on the console’s Deliveries page. If no endpoint is configured, the event is still recorded and remains readable through the activity log.
7 Errors
Every failure uses one envelope.
{
"error": "no_capacity",
"detail": "No free cell of the requested size at that station.",
"request_id": "8f2c4b1e9a7d43c6b0e5f1a29d3c7b84"
}| Field | Description |
|---|---|
error |
Stable machine-readable code. Branch on this. |
detail |
Human-readable explanation. Do not parse. |
request_id |
Correlation identifier. Quote it in support requests. |
fields |
Present on validation errors: field name to messages. |
request_id is also returned on success, in the
X-Request-Id response header.
7.1 Status codes
| Status | Meaning | Action |
|---|---|---|
200 / 201 |
Success | — |
400 |
Validation failure | Correct the request. Inspect
fields. |
401 |
Missing or expired token | Obtain a new token. |
403 |
Insufficient scope | Request the required scope. |
404 |
Not found for your organisation | Check the identifier. |
409 |
Conflicting state or no capacity | Retry with different parameters. |
422 |
Provisioning failed | Transient service issue; retry. |
429 |
Rate limited | Back off and retry. |
502 / 503 |
Service unavailable | Retry with backoff. |
7.2 Error codes
error |
Meaning |
|---|---|
validation_error |
A field is missing, malformed or not part of the contract. |
conflict |
An order with that reference already exists. |
no_capacity |
No free cell of the requested size. |
provisioning_failed |
A referenced record could not be created. |
not_found |
No such resource for your organisation. |
insufficient_scope |
The token lacks the required scope. |
upstream_unavailable |
The service is temporarily unavailable. |
upstream_rejected |
The service declined the operation. |
8 The data-acquisition model
This section documents how the API keeps your data yours, because the mechanism matters more than the assurance.
8.1 What is refused
The order endpoint accepts a closed set of fields. Anything outside
it is rejected with 400 and the offending field names
returned. customer_name, customer_phone,
address, email are not “ignored” — they cause
the request to fail. Your integration cannot accidentally leak personal
data into the API, because there is no code path that accepts it.
8.2 What is stored
The API’s schema contains no column for a name, telephone number, email address or postal address. The complete record of a party is:
| Column | Content |
|---|---|
external_id |
The identifier you chose,
e.g. CUST-9917 |
internal_id |
The API’s own record number |
pseudonym |
A keyed hash of your identifier |
provisioned_at |
When the record was created |
There is nowhere to put personal data even if someone tried.
8.3 How a party is represented
A party record requires a name, an email address and a telephone number, unique across the system. The API satisfies those constraints without involving a real person, by deriving every value from a pseudonym:
pseudonym = HMAC-SHA256(server_key, "customer:CUST-9917")[:12]
= "b039cbaa1024"
| Field | Value | Property |
|---|---|---|
username |
vf_cus_b039cbaa1024 |
Unique, stable, opaque |
name |
Vodafone Customer B039CB |
Meaningless to a reader |
email |
cus-b039cbaa1024@vodafone.invalid |
.invalid can never receive
mail (RFC 2606) |
mobile |
+201073806589 |
Derived from the hash; satisfies uniqueness |
employee_id |
b039cbaa1024 |
The pseudonym, used to find the record again |
The mapping is deterministic, so the same identifier always resolves to the same record and no duplicate is ever created. It is keyed, so your identifiers cannot be recovered from anything the API holds. And the addresses are undeliverable by construction, so no message can escape to a real recipient.
8.4 Consequences
- The link between an identifier and a person exists only in your systems.
- The API cannot re-identify anyone.
- A database disclosure exposes identifiers, not people.
- Your obligations under data-protection regimes are materially narrowed, because the personal data never leaves your control in the first place.
9 Extensibility
The API is designed to absorb change rather than transmit it.
9.1 A contract that does not move
Cell sizes, station types, billing models and status vocabularies evolve over time. The API keeps a stable contract over all of it: statuses are mapped to a fixed set, size names are canonicalised, and internal identifiers are never exposed. Such changes never reach your integration.
9.2 Adding capability
New capabilities reach you by extension, not migration:
- A new station type appears in
GET /api/v1/stations/with no client change at all. - A new cell size becomes an additional accepted
value for
cell_size. - A new identifier kind — a locker bank, a contract, a cost centre — follows the same resolution pattern already used for customers, couriers, billing plans and templates: send an identifier, get a resolved record.
- A new event is added to the webhook set; endpoints you have not configured are simply not called.
Additive changes do not break existing clients. Fields are added,
never repurposed, and the API is versioned under /api/v1/
so a breaking change would arrive as /api/v2/ alongside
it.
9.3 Multiple organisations
Each organisation is issued its own credentials, scopes, webhook endpoints and activity log. Data is partitioned per organisation at the query layer: one organisation cannot read another’s orders, and identifier collisions between organisations are impossible because mappings are namespaced. Onboarding a new partner requires no deployment.
9.4 Availability
The API keeps its own record of every order it creates. Order history, identifier mappings and the activity log stay queryable, and read endpoints serve stored data — so your reporting does not depend on any single component being reachable.
10 Appendix A — enumerations
Order status
| Value | Meaning |
|---|---|
CREATED |
Accepted, cell reserved, awaiting deposit |
DROPPED |
Parcel deposited, awaiting collection |
PICKED |
Parcel collected |
EXPIRED |
Collection window elapsed |
CANCELLED |
Withdrawn before collection |
FAILED |
Could not be completed |
Cell size — small, medium,
large, xlarge
Cell status — free,
occupied, reserved
Cell state — active,
not_active
Station status — ACTIVE,
INACTIVE
Date shortcuts — today,
yesterday, last_week, last_month,
last_year
11 Appendix B — quick reference
BASE=https://ecommerce.api.imbox.solutions
# 1. Token
TOKEN=$(curl -s -X POST $BASE/oauth2/token/ \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials | python -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
# 2. Capacity
curl -s "$BASE/api/v1/stations/" -H "Authorization: Bearer $TOKEN"
# 3. Create an order
curl -s -X POST "$BASE/api/v1/orders/" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"station_id":"MALL-MASR-001","customer_id":"CUST-1",
"courier_id":"CR-1","cell_size":"small","idempotency_key":"job-1"}'
# 4. Track
curl -s "$BASE/api/v1/orders/?customer_id=CUST-1" -H "Authorization: Bearer $TOKEN"
# 5. Audit
curl -s "$BASE/api/v1/logs/?order_id=VF-2026-0001" -H "Authorization: Bearer $TOKEN"Add -k while evaluating against the private certificate
authority, or supply --cacert ca.crt to verify
properly.
12 Appendix C — support
Quote the request_id from the error response, or the
X-Request-Id response header, in any support request. It
correlates your call with the API’s internal logs.
| Resource | Location |
|---|---|
| Console | https://ecommerce.api.imbox.solutions/console/ |
| Health | https://ecommerce.api.imbox.solutions/api/v1/health/ |