---
updatedAt: 2026-10-28T12:26:16.000Z
agentTools:
  projectIndex: https://developers.solidstudio.io/llms.txt
---

# 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.                                                                                                                                                                                                                                                                            |

```mermaid
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.

```bash
curl -s -H "Authorization: $API_KEY" \
  "https://<host>/backoffice/customers?email=jane&page=1&size=10"
```

```json
{
  "content": [
    {
      "email": "jane.doe@example.com",
      "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. |

```mermaid
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&#x20;**`tokenUid`**:** the token is created with type `RFID`, `contractId` = `tokenUid`, whitelist `NEVER`, and not valid.
* **Known&#x20;**`tokenUid`**:** `visualNumber` is updated.
* `owner`**&#x20;present:** the token is assigned to that owner. The previous owner's token copy is invalidated first.
* `owner`**&#x20;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.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. |

```bash
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"
```

```json
{ "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.

```bash
curl -s -X PATCH -H "Authorization: $API_KEY" \
  "https://<host>/backoffice/token/5f0c2a8e-7d1b-4c43-9e1a-0a4b8f2d6c11/block"
```

```json
{ "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:

| 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.                       |

```mermaid
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": [...] }`.

```bash
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](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.

```bash
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`**&#x20;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:

```mermaid
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 }
```

```bash
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

* [getting-started.md](getting-started.md)
* [authorization.md](authorization.md)
* [sessions-cdrs.md](sessions-cdrs.md)
* [payments-settlement.md](payments-settlement.md)
* [charging-network.md](charging-network.md)
* [tariffs-coupons.md](tariffs-coupons.md)
* [bulk-operations.md](bulk-operations.md)