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

ConceptWhat it isIdentifier
CustomerA 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.
TokenA 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)
OrganizationA company that groups customers (members) and can own tokens.id, plus a generated organizationCode
Join requestA customer's request to join an organization, submitted from the app and processed in the back office.requestId
TagA free-text label assigned to a resource id (customer, organization, tariff, charging pool/point/connector).name (unique)
Prepaid wallet accountA 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.

statusMeaning
ACTIVERole USER. The customer can log in and charge.
BLOCKEDRole 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.
UNKNOWNNeither 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-versionPath value is interpreted as
absent / 1.0 (default)The identity-provider id (userId in the customer list). Marked for removal in code.
2.0customerId
anything else400 "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

typeMeaning
RFIDPhysical card. The only type the back-office API creates.
APP_USERApp-generated token of a customer. Read-only here.
AD_HOC_USER, OTHERRoaming token types (OCPI).

status is derived. It is not stored:

statusCondition
ACTIVEAssigned to a customer and valid.
BLOCKEDAssigned to a customer and not valid.
INACTIVENo 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

One idempotent upsert, keyed by tokenUid:

  • Unknown tokenUid: the token is created with type RFID, contractId = tokenUid, whitelist NEVER, and not valid.
  • Known tokenUid: visualNumber is updated.
  • owner present: the token is assigned to that owner. The previous owner's token copy is invalidated first.
  • owner omitted, token has a customer: the customer and organization are removed and the token becomes INACTIVE.
  • Owner unchanged: sending the current owner again changes nothing. It does not unblock a blocked token.

Validation:

  • tokenUid is required, max 36 characters.
  • visualNumber is max 64 characters and must not be blank if present.
  • A visualNumber already used by another UID is rejected.
owner.typeBodyEffect
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. Add exactUid=true for 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: organizationCode is generated from the name. An ORGANIZATION prepaid-wallet account is opened automatically.
  • Update: PUT replaces 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:

StatusMeaning
PENDINGSubmitted by the customer, not yet routed.
ASSIGNEDRouted to an organization by a SUPER_ADMIN.
APPROVEDAccepted. The customer is added as a member and notified.
REJECTEDDeclined. 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 Content

Once 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/assign with { "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.

OperationDebit → creditPrecondition
POST /direct-funding/user-by-systemSYSTEM → USERSystem debit limit. The customer is notified.
POST /direct-funding/organization-by-systemSYSTEM → ORGANIZATIONSystem debit limit.
POST /direct-funding/user-by-organizationORGANIZATION → USERSufficient 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 to totalBalance minus activeFundsHoldAmount.
  • 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


Did this page help you?