---
title: Build for your users
description: Build an app or a portal on the End-User API: sign a user in, show their subscriptions, and sell a change through an order
---

Build an application that acts for one of your users. The user signs in, reads their
subscriptions and their remaining data, buys a topup, changes their plan, and protects a lost
SIM. Every request goes to the [End-User API](/end-user-api.md), which limits the request to what
that user can see and do.

## Prerequisites

You need all of these before you start:

- **Impersonation key**: An API key with the access type **Impersonate user**, created in the portal
- **Users with customers**: Each user belongs to one customer or more, and each customer has subscriptions
- **Payment provider**: A configured payment provider, for an order that costs money

Every request sends the key in `X-API-Key` and the token of the user in `Authorization: Bearer`.
Read [End-User authentication](/end-user-api/authentication.md) for the login flow and for what the
API refuses. The base URL is `{BASE_URL}/api/v2/end-user`.

## Overview

**1. Sign the user in**

Start the email login, then verify the code.

**2. Load the user and their customers**

Read the profile, then the customers that the user can act for.

**3. Show subscriptions and usage**

List the subscriptions of a customer, then read the usage of each one.

**4. Sell a change through an order**

Create an order, add a line item, then collect the payment.

**5. Protect the line**

Block a lost SIM, or pause and restore a subscription.

## Step 1: Sign the user in

Start the login with `POST /auth/email/start`, then verify the code with `POST /auth/email/verify`.
The response carries the `accessToken` of the user. [End-User
authentication](/end-user-api/authentication.md) has the full flow with samples.

## Step 2: Load the user and their customers

Read the profile of the user. The `customers` array names each customer that the user can act for:

```bash
curl "{BASE_URL}/api/v2/end-user/accounts/me" \
  -H "X-API-Key: $API_KEY" \
  -H "Authorization: Bearer $USER_TOKEN"
```

```json
{
  "userId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "name": "Emma Johnson",
  "email": "emma.johnson@example.com",
  "customers": [
    {
      "customerId": "c2d1a3e4-1b2c-4d5e-8f90-123456789abc",
      "name": "Emma Johnson",
      "customerType": "CONSUMER"
    }
  ]
}
```

A user with more than one customer picks one. `GET /customers` lists the same customers with
their full details. `GET /customers/{customerId}/product-catalog` gives the product offerings
that this customer can buy.

## Step 3: Show subscriptions and usage

List the subscriptions of the customer, then read one subscription for its detail page:

```bash
curl "{BASE_URL}/api/v2/end-user/subscriptions?customerId=c2d1a3e4-1b2c-4d5e-8f90-123456789abc" \
  -H "X-API-Key: $API_KEY" \
  -H "Authorization: Bearer $USER_TOKEN"

curl "{BASE_URL}/api/v2/end-user/subscriptions/d8174435-6378-4be5-a9f5-8b4aaadae5d4" \
  -H "X-API-Key: $API_KEY" \
  -H "Authorization: Bearer $USER_TOKEN"
```

The subscription `status` is one of `PENDING`, `ACTIVATED`, `BLOCKED`, `CANCELLED`, `PAUSED`,
or `SUSPENDED`. The response of each subscription has the same shape as on the Platform API.
The [Customer self-service](/developer-guide/use-cases/self-management.md) guide shows a full
subscription and a full usage response.

Read the usage of the current period to show the data, voice, and SMS meters:
```bash
curl "{BASE_URL}/api/v2/end-user/subscriptions/d8174435-6378-4be5-a9f5-8b4aaadae5d4/usage" \
  -H "X-API-Key: $API_KEY" \
  -H "Authorization: Bearer $USER_TOKEN"
```

For an eSIM subscription, `GET /subscriptions/{subscriptionId}/esim/qrcode` gives the activation
code and a QR code image.

> **Warning**
>
> Do not cache a QR code. Request a new one when the user opens the installation screen. Each code
> expires at its `expiresAt`, and anybody who scans it can install the eSIM profile.

## Step 4: Sell a change through an order

On the End-User API, every change that costs money goes through an order. A topup, a plan change,
and a new subscription are each one line item on an order. The order prices the change, and a
payment session collects the money.

First read what the user can buy. `GET /subscriptions/{subscriptionId}/product-offering-options`
lists the plans that a subscription can change to, with the date each change takes effect.
`GET /subscriptions/{subscriptionId}/addon-options` lists the addons and the topups.

Then create the order for the customer, and add the line item:

```bash
# Create the order
curl -X POST "{BASE_URL}/api/v2/end-user/orders" \
  -H "X-API-Key: $API_KEY" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "customerType": "CONSUMER",
    "customer": { "customerId": "c2d1a3e4-1b2c-4d5e-8f90-123456789abc" }
  }'

# Add a topup to a subscription
curl -X POST "{BASE_URL}/api/v2/end-user/orders/9b2e4f10-7c3d-4a1e-b5f6-0d8c7e6a5b4c/line-items" \
  -H "X-API-Key: $API_KEY" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "lineItem": {
      "type": "ADDON",
      "lineItemId": "topup-0",
      "productOfferingId": "addon-data-5gb",
      "subscriptionId": "d8174435-6378-4be5-a9f5-8b4aaadae5d4"
    }
  }'
```

A plan change is a `SUBSCRIPTION_CHANGE` line item instead:
```json
{
  "lineItem": {
    "type": "SUBSCRIPTION_CHANGE",
    "lineItemId": "change-0",
    "subscriptionId": "d8174435-6378-4be5-a9f5-8b4aaadae5d4",
    "changeType": "PLAN_CHANGE",
    "planChange": { "productOfferingId": "cell-unlimited" }
  }
}
```

Then read the order. The `pricing.totalMinor` field tells you what happens next:

- If the total is `0`, submit the order with `POST /orders/{orderId}/submit`. A cheaper plan prices
  to zero, because the order charges only the difference for the days already paid.
- If the total is more than `0`, create a payment session, then send the user to its hosted page:
```bash
curl -X POST "{BASE_URL}/api/v2/end-user/payment-sessions" \
  -H "X-API-Key: $API_KEY" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": "9b2e4f10-7c3d-4a1e-b5f6-0d8c7e6a5b4c",
    "paymentProvider": "STRIPE",
    "hosted": true,
    "returnUrl": "https://app.example.com/orders/9b2e4f10-7c3d-4a1e-b5f6-0d8c7e6a5b4c"
  }'
```

The response carries `hostedUrl`. When the payment succeeds, the order submits itself. Poll
`GET /orders/{orderId}` until `state` is `COMPLETED`, or read the status from the order page of the
user.

> **Note**
>
> An order with no `pricing` at all is one that the API did not price. Do not submit it. Show the
> user an error, and read the `validation` field of the order for the cause.

### Hand the order to the checkout

You can let the hosted checkout collect the payment instead of your application. Your
application builds the order. The checkout prices the order again, collects the payment, and
submits the order. Ask us for the checkout URL of your brand.

**1. Build the order**

Create the order and add the line items, as the previous section shows. Do not create a payment
session.

**2. Create a handoff token**

Send `POST /orders/{orderId}/handoff-token`.

**3. Send the user to the checkout**

Open `{CHECKOUT_URL}/storefront/checkout?claim={handoffToken}` in the browser of the user.

**4. Wait for the order**

Poll `GET /orders/{orderId}` until `state` is `COMPLETED`.

```bash
curl -X POST "{BASE_URL}/api/v2/end-user/orders/9b2e4f10-7c3d-4a1e-b5f6-0d8c7e6a5b4c/handoff-token" \
  -H "X-API-Key: $API_KEY" \
  -H "Authorization: Bearer $USER_TOKEN"
```

```json
{
  "handoffToken": "oht_f47ac10b58cc4372a5670e02b2c3d479",
  "orderId": "9b2e4f10-7c3d-4a1e-b5f6-0d8c7e6a5b4c",
  "expiresAt": "2026-07-15T11:00:00Z"
}
```

#### What the checkout does

When the user opens the link, the checkout does these steps:

1. It sends the token to `POST /orders/claim-handoff` and gets the order.
2. It replaces the cart of the user with this order.
3. It asks the user to log in, if the user is not logged in to the checkout.
4. It reads the price of the order again. It does not use the price that your application showed.
5. It collects the payment. If the total is `0`, it submits the order without a payment.

Your application does not call `POST /orders/claim-handoff`. Only a checkout calls it.

#### Token rules

| Rule        | Value                                                                                     |
| ----------- | ----------------------------------------------------------------------------------------- |
| Lifetime    | 1 hour. The `expiresAt` field gives the exact time.                                       |
| Order state | The order must be `PENDING` or `PENDING_PAYMENT`.                                         |
| More tokens | Each request creates a new token. The earlier tokens stay valid until they expire.        |
| Reuse       | A claim does not consume the token. The token works until it expires or the order closes. |
| Lost token  | We keep only a hash of the token. Create a new token.                                     |
| Revocation  | No operation revokes a token. A submitted or cancelled order stops all of its tokens.     |

#### Errors

| Status | Operation                              | Cause                                            |
| ------ | -------------------------------------- | ------------------------------------------------ |
| `404`  | `POST /orders/{orderId}/handoff-token` | The order does not exist.                        |
| `404`  | `POST /orders/claim-handoff`           | The token is unknown or expired.                 |
| `412`  | Both                                   | The order is not `PENDING` or `PENDING_PAYMENT`. |

> **Warning**
>
> Give the token only to the user that the order is for. A person with the token can see, change,
> and pay for the order.

## Step 5: Protect the line

When the user reports a lost or stolen SIM, block it at once:
```bash
curl -X POST "{BASE_URL}/api/v2/end-user/subscriptions/d8174435-6378-4be5-a9f5-8b4aaadae5d4/block-sim" \
  -H "X-API-Key: $API_KEY" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "X-Idempotency-Key: block-1d7f9c3b"
```

A block takes effect at once, and the user cannot reverse it. Only a new SIM card brings the
subscription back into service, and the End-User API does not issue one. Tell the user to
contact the operator.

A pause is different. The user stops the service and the payments for a time, then restores
them:
```bash
# Pause from the first day of the next month
curl -X POST "{BASE_URL}/api/v2/end-user/subscriptions/d8174435-6378-4be5-a9f5-8b4aaadae5d4/pause" \
  -H "X-API-Key: $API_KEY" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "scheduledAt": "2026-08-01" }'

# Restore at once
curl -X POST "{BASE_URL}/api/v2/end-user/subscriptions/d8174435-6378-4be5-a9f5-8b4aaadae5d4/restore" \
  -H "X-API-Key: $API_KEY" \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

The user can restore only a pause that the user made. A suspension by the operator, for
non-payment or for fraud, stays until the operator lifts it.

## Tools

The `/tools` operations help the user before a purchase. Each one takes the same two
credentials:

- `POST /tools/check-porting-eligibility` tells whether a number can move to your network
- `POST /tools/check-network-coverage` tells whether an address has coverage
- `POST /tools/validate-address` corrects and completes a postal address
- `POST /tools/search-devices` and `POST /tools/get-device-info` find a device and its eSIM support

## What stays on the Platform API

The End-User API has no invoices, no saved payment methods, no user management, and no
cancellation. Do these from your own backend with a full-access key on the [Platform
API](/api-reference.md), as the [Customer self-service](/developer-guide/use-cases/self-management.md)
guide shows. Keep the full-access key on your backend. It never belongs in an app that you
distribute.

## Error handling

A `403 Forbidden` with `internalCode` `4337` means that the key has full API access. A `401
Unauthorized` means that the user token is absent, invalid, or expired. Start a new login. For
every other error, read [Errors](/api-reference/errors.md).

## Next steps

- Read the [End-User API reference](/end-user-api.md) for every operation and schema.
- Read [Versioning](/api-reference/versioning.md) before you pin your key to a revision.
- Read [Idempotency](/api-reference/idempotency.md) before you retry an order request.
