Customers & tokens
How EMSP customers, their charging tokens, organizations, tags and prepaid wallets appear in the EMSP Public API, and which back-office actions exist on them.
Model
| Concept | What it is | Identifier |
|---|---|---|
| Customer | A registered driver (mobile app user). Created by app sign-up, not by the back-office API. | customerId (internal id). The customer list also returns userId = the identity-provider id. |
| Token | A credential that authorizes charging. RFID cards are managed here. App tokens (APP_USER) are created by the app and can only be read. | id (internal id), uId (token UID), visualNumber (number printed on the card) |
| Organization | A company that groups customers (members) and can own tokens. | id, plus a generated organizationCode |
| Join request | A customer's request to join an organization, submitted from the app and processed in the back office. | requestId |
| Tag | A free-text label assigned to a resource id (customer, organization, tariff, charging pool/point/connector). | name (unique) |
| Prepaid wallet account | A ledger account per owner and currency. | accountId (UUID) |
Two customer ids exist. Use customerId everywhere unless stated otherwise. userId in the customer list is the identity-provider id.
Customers
Customer status
The status comes from the customer's role.
status | Meaning |
|---|---|
ACTIVE | Role USER. The customer can log in and charge. |
BLOCKED | Role SUSPENDED_USER. The customer is disabled in the identity provider. RFID authorization is rejected with "user is blocked" when the UNBLOCKED_USER token validation strategy is active. App tokens are reported as invalid. Prepaid-wallet auto top-up is switched off on its next run. |
UNKNOWN | Neither role found. |
stateDiagram-v2
[*] --> ACTIVE: app registration
ACTIVE --> BLOCKED: POST /customers/{id}/block
BLOCKED --> ACTIVE: POST /customers/{id}/unblock
Block and unblock are idempotent. Blocking a blocked customer, or unblocking an active one, does nothing and returns 200.
POST /backoffice/customers/{customerId}/block reads the optional x-api-version header:
x-api-version | Path value is interpreted as |
|---|---|
absent / 1.0 (default) | The identity-provider id (userId in the customer list). Marked for removal in code. |
2.0 | customerId |
| anything else | 400 "Not supported API version. Expected one of (1.0, 2.0)" |
unblock always takes customerId. Send x-api-version: 2.0 on block so both calls use the same id.
Look up a customer
The list returns only registered customers (role USER or SUSPENDED_USER) that are not deleted. Results are sorted by joinedAt descending. Filters:
email: case-insensitive substring.rfid=true: customers with at least one valid RFID token.rfid=false: customers with no valid RFID token.
curl -s -H "Authorization: $API_KEY" \
"https://<host>/backoffice/customers?email=jane&page=1&size=10"{
"content": [
{
"email": "[email protected]",
"userId": "eu-west-1:0b6c1e3a-0000-4000-8000-000000000001",
"customerId": "36deeefc-2eb1-4329-98a9-600b8cc1d62d",
"status": "ACTIVE",
"hasActiveRfid": true,
"joinedAt": "2026-03-14T09:12:00Z",
"autochargeEnabled": false,
"tags": ["fleet-pilot"]
}
],
"totalElements": 1,
"totalPages": 1,
"currentPage": 1
}GET /backoffice/customers/{customerId} returns the profile. It includes the private and business profile fields, organizationId/organizationName, tags, externalReference and card expiry info. An unknown id returns 404 with an empty body.
PUT /backoffice/customers/{customerId}/external-reference with { "externalReference": "CRM-1234" } links the customer to your own system (max 255 characters). Returns 204.
Tokens
Token types and statuses
type | Meaning |
|---|---|
RFID | Physical card. The only type the back-office API creates. |
APP_USER | App-generated token of a customer. Read-only here. |
AD_HOC_USER, OTHER | Roaming token types (OCPI). |
status is derived. It is not stored:
status | Condition |
|---|---|
ACTIVE | Assigned to a customer and valid. |
BLOCKED | Assigned to a customer and not valid. |
INACTIVE | No customer assigned and not valid, for example a new unassigned card or a card owned only by an organization. |
stateDiagram-v2
[*] --> INACTIVE: PUT /v3/backoffice/token (no owner or Organization owner)
[*] --> ACTIVE: PUT /v3/backoffice/token (Customer or OrganizationMember owner)
INACTIVE --> ACTIVE: PUT with Customer / OrganizationMember owner
ACTIVE --> BLOCKED: PATCH /backoffice/token/{id}/block
BLOCKED --> ACTIVE: PATCH /backoffice/token/{id}/unblock
ACTIVE --> INACTIVE: PUT without owner (unassign)
BLOCKED --> INACTIVE: PUT without owner (unassign)
Every token write is forwarded to the OCPI adapter. The token is sent as valid only when its status is ACTIVE. Writes are upsert, block, unblock and unassign.
Issue or reassign a card: PUT /v3/backoffice/token
PUT /v3/backoffice/tokenOne idempotent upsert, keyed by tokenUid:
- Unknown
tokenUid: the token is created with typeRFID,contractId=tokenUid, whitelistNEVER, and not valid. - Known
tokenUid:visualNumberis updated. ownerpresent: the token is assigned to that owner. The previous owner's token copy is invalidated first.owneromitted, token has a customer: the customer and organization are removed and the token becomesINACTIVE.- Owner unchanged: sending the current owner again changes nothing. It does not unblock a blocked token.
Validation:
tokenUidis required, max 36 characters.visualNumberis max 64 characters and must not be blank if present.- A
visualNumberalready used by another UID is rejected.
owner.type | Body | Effect |
|---|---|---|
Customer | { "type": "Customer", "id": "<customerId>" } | Token belongs to the customer, organization cleared, valid → ACTIVE. |
Organization | { "type": "Organization", "id": "<organizationId>" } | Token without a customer: owned by the organization, not valid → INACTIVE. Token with a customer: treated as OrganizationMember with that customer. |
OrganizationMember | { "type": "OrganizationMember", "userId": "<customerId>", "organizationId": "<organizationId>" } | Customer and organization set, valid → ACTIVE. The customer must already be a member of that organization. Otherwise the organization is reported as not found. |
curl -s -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" \
-d '{
"tokenUid": "12345678905880",
"visualNumber": "DF000-2001-8999-1",
"owner": { "type": "Customer", "id": "36deeefc-2eb1-4329-98a9-600b8cc1d62d" }
}' \
"https://<host>/v3/backoffice/token"{ "id": "5f0c2a8e-7d1b-4c43-9e1a-0a4b8f2d6c11" }Block or unblock a token
The token needs an assigned customer who is not deleted. Otherwise the call returns 400. Both calls are idempotent and return the resulting status.
curl -s -X PATCH -H "Authorization: $API_KEY" \
"https://<host>/backoffice/token/5f0c2a8e-7d1b-4c43-9e1a-0a4b8f2d6c11/block"{ "tokenId": "5f0c2a8e-7d1b-4c43-9e1a-0a4b8f2d6c11", "newStatus": "BLOCKED" }Search tokens
GET /backoffice/token filters:
uid: fuzzy match. AddexactUid=truefor an exact match.visualNumber,userId(customer id),email.type,status.corporateName: the organization's corporate name.
Use GET /backoffice/token/{id} for one token. Use GET /backoffice/token/app-user/USERID for a customer's app token: it returns { "userId", "tokenId" }, where tokenId is the app token UID.
Organizations
An organization has profile data and a set of members. Profile data: name, corporateName, taxId, address, email, phone, corporatePaymentEnabled (default true), externalReference. It can also have admin users, given as identity-provider ids in adminsExternalIds, and tags.
- Create:
organizationCodeis generated from the name. AnORGANIZATIONprepaid-wallet account is opened automatically. - Update:
PUTreplaces the profile, the admins and the tag set.
There are two ways to add a member.
Direct. POST /backoffice/organizations/{organizationId}/users/{customerId} adds a customer immediately. DELETE on the same path removes the customer. If the customer is already a member, or not a member, the call fails.
Join request. The customer submits a request from the app. The back office then moves it through these statuses:
| Status | Meaning |
|---|---|
PENDING | Submitted by the customer, not yet routed. |
ASSIGNED | Routed to an organization by a SUPER_ADMIN. |
APPROVED | Accepted. The customer is added as a member and notified. |
REJECTED | Declined. The customer is notified. |
stateDiagram-v2
[*] --> PENDING: customer submits in app
PENDING --> ASSIGNED: PUT /join-requests/{id}/assign/{organizationId}
PENDING --> REJECTED: POST /join-requests/{id}/reject
ASSIGNED --> APPROVED: POST /join-requests/{id}/approve
ASSIGNED --> REJECTED: POST /join-requests/{id}/reject
Only an ASSIGNED request can be approved. Any other transition is rejected. Bulk variants are POST /join-requests/approve, POST /join-requests/reject and DELETE /join-requests, all with body { "requestIds": [...] }.
curl -s -X POST -H "Authorization: $API_KEY" \
"https://<host>/backoffice/organizations/8346f519-79c5-4ea3-bb5c-eeadb8eb5422/users/36deeefc-2eb1-4329-98a9-600b8cc1d62d"
# 204 No ContentOnce a customer is a member, issue them a company card with owner.type = OrganizationMember. Members' CDRs are available under GET /backoffice/organizations/{organizationId}/cdrs and /cdrs/summary, filtered by dateFrom/dateTo (both required). See sessions-cdrs.md.
Tags
A tag is a unique name. Assigning tags to a resource id is a full replace:
PUT /backoffice/tags/assignwith{ "resourceId", "tags": [...] }deletes the resource's current tags and writes the new set.- Tag names that do not exist yet are created.
- An empty set clears all tags.
Organization create/update set tags the same way. Customer and organization responses include their tag names.
curl -s -X PUT -H "Authorization: $API_KEY" -H "Content-Type: application/json" \
-d '{ "resourceId": "36deeefc-2eb1-4329-98a9-600b8cc1d62d", "tags": ["fleet-pilot", "vip"] }' \
"https://<host>/backoffice/tags/assign"POST /backoffice/tags creates a tag. It fails with 400 if the name is taken. PUT /backoffice/tags/{tagName} renames a tag and DELETE /backoffice/tags/{tagName} deletes it. GET /backoffice/tags?search= searches by partial name. Max 255 characters per name.
Prepaid wallet
Each customer (USER) and organization (ORGANIZATION) gets a wallet account when it is created. One SYSTEM account per currency is the funding source. Balances change only through ledger transactions. Every transaction debits one account and credits another.
| Operation | Debit → credit | Precondition |
|---|---|---|
POST /direct-funding/user-by-system | SYSTEM → USER | System debit limit. The customer is notified. |
POST /direct-funding/organization-by-system | SYSTEM → ORGANIZATION | System debit limit. |
POST /direct-funding/user-by-organization | ORGANIZATION → USER | Sufficient organization balance. The customer is notified. |
All funding calls require amount, currency (ISO 4217) and externalReference. externalReference is the idempotency key. Repeating a call with a reference that already exists returns the existing transaction and posts nothing new. Account type and currency must match the account. Otherwise the call returns 404.
Typical adjustment flow:
sequenceDiagram
autonumber
participant C as Your backend
participant API as EMSP Public API
C->>API: GET /backoffice/prepaid-wallet/accounts?ownerId={customerId}
API-->>C: [{ id, type: USER, currency, balance, ... }]
C->>API: POST /backoffice/prepaid-wallet/direct-funding/user-by-system<br/>{ userAccountId, amount, currency, externalReference }
API-->>C: 200
C->>API: GET /backoffice/prepaid-wallet/accounts/{accountId}/transactions
API-->>C: Page of { id, description, currency, amount, externalReference, date }
curl -s -X POST -H "Authorization: $API_KEY" -H "Content-Type: application/json" \
-d '{
"userAccountId": "9d6ef5e8-0ed7-4d07-a4c1-3ef2f31a7a27",
"amount": 25.00,
"currency": "EUR",
"externalReference": "goodwill-2026-10-27-0001"
}' \
"https://<host>/backoffice/prepaid-wallet/direct-funding/user-by-system"GET /accounts?ownerId= returns these values per account:
balance: available balance, equal tototalBalanceminusactiveFundsHoldAmount.totalBalance: the full ledger balance.activeFundsHoldAmount: funds reserved by active holds.fundsHoldCapability: whether a funds hold can be reserved now.
GET /accounts/{accountId}/balance returns { "balance" }. GET /balance-report?dateTo=&dateFrom= streams a CSV with columns userId,userEmail,balance,date for all customer wallets. GET /currencies lists the supported currencies.
Related
Updated 14 days ago