# Users

End-users and their Static Wallet Address — one permanent address per user, reused for every payment

3 endpoints. Generated from `docs/openapi.yaml` v1.0.0.

| Endpoint | Summary |
| --- | --- |
| [`GET /v1/users`](#get-v1-users) | List your users |
| [`POST /v1/users`](#post-v1-users) | Create a user and derive its address |
| [`GET /v1/users/{externalId}/addresses`](#get-v1-users-externalid-addresses) | Read a user's Static Wallet Address |

## GET /v1/users

**List your users**  ·  _Auth: bearer API key_  ·  `operationId: listUsers`

Your users, newest first (by creation time). Scoped to your tenant — nobody
else's users are ever visible.

`total` is counted through the SAME filter as the rows, so it is the size of
the filtered set, not of your whole user base: page with `offset` until you
have `total` rows. The page is ordered on figures the database cannot compute
(a USD total needs the price feed), so at most 2000 matching users are read;
`total` counts no further than that, and `truncated: true` says the cap was
hit — narrow the filter.

`balance_usd`, `balances[].usd` and `card_balance` are price-feed estimates
for display, with no `_base` twin; read exact balances from
GET /v1/users/{externalId}/balances.

Reserved internal accounts (the card-programme float pot) are excluded,
so this list agrees with the user counts your dashboard shows.

`from`/`to` accept either a bare IST calendar day (`YYYY-MM-DD`, the whole day
INCLUDED on both ends) or a full RFC 3339 instant with an offset. Anything else
is a `400` rather than a silently ignored filter.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `search` | query | `string` | no | Case-insensitive substring match on `external_user_id`. |
| `from` | query | `string` | no | Only users created on/after this IST day (or instant). |
| `to` | query | `string` | no | Only users created on/before this IST day (or before this instant). |
| `limit` | query | `integer` | no | — _(default `50`; min `1`; max `200`)_ |
| `offset` | query | `integer` | no | — _(default `0`; min `0`)_ |
| `sort` | query | `string` | no | What orders the page — when the user was created, what they hold (USD estimate), or when they last moved money. A user who never moved money sorts last either way. _(one of `created`, `balance`, `last_active`; default `"created"`)_ |
| `dir` | query | `string` | no | — _(one of `asc`, `desc`; default `"desc"`)_ |

#### Responses

| Status | Body | Meaning |
| --- | --- | --- |
| `200` | `object` | One page of users, plus the total matching the same filter. |
| `400` | `Error` | Body/params failed validation, or a domain rule was violated. `error` is one of: validation_error, unknown_asset, asset_disabled, invalid_amount, amount_below_minimum, bad_amount, invalid_address, invalid_request. |
| `401` | `Error` | Missing, malformed or unknown API key. |

**`200` response body**

| Field | Type | Description |
| --- | --- | --- |
| `users` **required** | `object[]` | — |
| `users[].user_id` **required** | `string` | Your `external_user_id` for this user. |
| `users[].account_kind` **required** | `string` | `end_user` is one of YOUR customers; `merchant_own` is an account that holds your own money (e.g. your treasury). _(one of `end_user`, `merchant_own`)_ |
| `users[].created_at` **required** | `string<date-time>` | — |
| `users[].balance_usd` **required** | `string` | USD estimate of everything the user holds, two decimals. Display only. |
| `users[].balances` **required** | `object[]` | Per-asset holdings, largest USD value first. |
| `users[].balances[].asset_id` **required** | `string` | — |
| `users[].balances[].symbol` **required** | `string` | — |
| `users[].balances[].amount` **required** | `string` | A monetary value as a decimal string. Always paired with a `*_base` field that carries the exact integer base-unit value; compute with the `_base` string. |
| `users[].balances[].usd` **required** | `string` | Price-feed estimate; no `_base` twin. |
| `users[].card_balance` **required** | `string | null` | Sum on the user's cards (available + pending); null when the user has no card. |
| `users[].last_active_at` **required** | `string<date-time> | null` | The user's most recent ledger movement; null when none. |
| `total` **required** | `integer` | Users matching the filter, ignoring `limit`/`offset` — counted no further than 2000. |
| `truncated` **required** | `boolean` | true when more than 2000 users matched and `total` stopped there. Narrow the filter. |
| `limit` **required** | `integer` | — |
| `offset` **required** | `integer` | — |

```json
{
  "users": [
    {
      "user_id": "user-42",
      "account_kind": "end_user",
      "created_at": "2026-08-31T09:15:00.000Z",
      "balance_usd": "125.40",
      "balances": [
        {
          "asset_id": "usdt_trc20",
          "symbol": "USDT",
          "amount": "1.5",
          "usd": null
        }
      ],
      "card_balance": "string",
      "last_active_at": "2026-08-31T09:15:00.000Z"
    }
  ],
  "total": 1,
  "truncated": true,
  "limit": 1,
  "offset": 1
}
```

#### Example request

```bash
curl -X GET 'https://api.cryptoinfra.build/v1/users?from=2026-08-01&to=2026-08-31' \
  -H 'Authorization: Bearer ck_live_your_key'
```

## POST /v1/users

**Create a user and derive its address**  ·  _Auth: bearer API key_  ·  `operationId: createUser`

Creates a user under your tenant and derives their Static Wallet Address — one permanent TRON address.
Every asset in GET /v1/assets lands on that one address; `addresses` is a list
so a client reads it the same way whatever it holds. Idempotent on
`external_user_id`: replaying returns the existing user and address (still `201`).

#### Request body

`application/json` · required

| Field | Type | Description |
| --- | --- | --- |
| `external_user_id` **required** | `string` | A number of 5 to 10 digits — the ID this user has in your own system. Letters are refused. _(minLength `5`; maxLength `10`; pattern `^[0-9]{5,10}$`)_ |

#### Responses

| Status | Body | Meaning |
| --- | --- | --- |
| `201` | `UserAddresses` | User created (or already existed). |
| `400` | `Error` | Body/params failed validation, or a domain rule was violated. `error` is one of: validation_error, unknown_asset, asset_disabled, invalid_amount, amount_below_minimum, bad_amount, invalid_address, invalid_request. |
| `401` | `Error` | Missing, malformed or unknown API key. |

**`201` response body** — `UserAddresses`

| Field | Type | Description |
| --- | --- | --- |
| `user_id` **required** | `string` | Your external_user_id. |
| `addresses` **required** | `object[]` | — |
| `addresses[].chain` **required** | `string` | The chain an address or asset is on. TRON is the only network the platform serves: `btc`, `eth`, `bsc` and `sol` were retired — historical addresses on them are never handed out again and nothing scans them. Read the value as an open string rather than a closed list, so a chain added later does not break your client. _(one of `tron`)_ |
| `addresses[].address` **required** | `string` | The Static Wallet Address — permanent |

```json
{
  "user_id": "string",
  "addresses": [
    {
      "chain": "tron",
      "address": "string"
    }
  ]
}
```

#### Example request

```bash
curl -X POST 'https://api.cryptoinfra.build/v1/users' \
  -H 'Authorization: Bearer ck_live_your_key' \
  -H 'Content-Type: application/json' \
  -d '{
  "external_user_id": "10482"
}'
```

## GET /v1/users/{externalId}/addresses

**Read a user's Static Wallet Address**  ·  _Auth: bearer API key_  ·  `operationId: getUserAddresses`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `externalId` | path | `string` | yes | Your `external_user_id` for the user. |

#### Responses

| Status | Body | Meaning |
| --- | --- | --- |
| `200` | `UserAddresses` | The user's Static Wallet Address, as a one-entry list keyed by chain. |
| `401` | `Error` | Missing, malformed or unknown API key. |
| `404` | `Error` | Unknown external_user_id for this tenant. |

**`200` response body** — `UserAddresses`

| Field | Type | Description |
| --- | --- | --- |
| `user_id` **required** | `string` | Your external_user_id. |
| `addresses` **required** | `object[]` | — |
| `addresses[].chain` **required** | `string` | The chain an address or asset is on. TRON is the only network the platform serves: `btc`, `eth`, `bsc` and `sol` were retired — historical addresses on them are never handed out again and nothing scans them. Read the value as an open string rather than a closed list, so a chain added later does not break your client. _(one of `tron`)_ |
| `addresses[].address` **required** | `string` | The Static Wallet Address — permanent |

```json
{
  "user_id": "string",
  "addresses": [
    {
      "chain": "tron",
      "address": "string"
    }
  ]
}
```

#### Example request

```bash
curl -X GET 'https://api.cryptoinfra.build/v1/users/string/addresses' \
  -H 'Authorization: Bearer ck_live_your_key'
```
