---
title: Customer Identification
description: Two identifier types and two URL forms for customers — when to use each, and how to avoid mixing them.
order: 9
---

# Customer Identification

There are two ways to identify a customer in QuotaStack. Use whichever is convenient. Never mix them in one request.

> **Mental Model:** Customers have **two names**: the one you already use (`external_customer_id`) and the one QuotaStack generates (`customer_id` UUID). Use either. Most apps only ever need the one they already have.

## Quick Take

- Two ID types: **your tenant ID** (`external_customer_id`) or the **QuotaStack UUID** (`customer_id`)
- Two URL families: `/v1/customers/{id}/...` vs `/v1/customer-by-external-id/{ext_id}/...`
- Body endpoints accept one of the two fields — **never both, never neither**
- **URL-encode** special chars (`:`, `/`, spaces) when using external IDs in URL paths

## Diagram

Every customer has two identifiers and every customer-scoped endpoint has two URL forms. For body-based endpoints, you supply exactly one identifier field. For URL-based endpoints, you pick the matching path family.

| Your input | Body field | URL path |
|---|---|---|
| Your tenant ID (e.g. `user_abc`) | `external_customer_id` | `/v1/customer-by-external-id/{id}/...` |
| QuotaStack UUID | `customer_id` | `/v1/customers/{id}/...` |

## Two identifier types

| Type | Field name | URL prefix | Owner |
|---|---|---|---|
| QuotaStack UUID | `customer_id` | `/v1/customers/{id}` | QuotaStack |
| Your tenant ID | `external_customer_id` | `/v1/customer-by-external-id/{external_id}` | You |

`external_customer_id` is whatever your system already calls the user — an email, a database row ID, a compound key like `userId:companionId`. QuotaStack stores it verbatim and you can query by it directly.

Most tenants **never need the UUID**. They supply `external_customer_id` everywhere and pretend QuotaStack's UUID doesn't exist. That's fine — both forms are fully supported.

## Body endpoints

Four endpoints accept either field in the request body. Exactly one required (both or neither returns `400`):

```
POST /v1/usage
POST /v1/usage/batch
POST /v1/reserve
POST /v1/topup/grant
```

Recommended — use your own ID:

```json
{
  "external_customer_id": "userId:companionId",
  "credits": 50000,
  "currency": "INR"
}
```

Alternative — use the QuotaStack UUID:

```json
{
  "customer_id": "019d6258-07ba-7418-83be-58f5fde53e4e",
  "credits": 50000,
  "currency": "INR"
}
```

For `POST /v1/usage/batch`, each event in the array picks its own field independently. You can mix them across events, just not within a single event.

## URL endpoints

Every customer-scoped path has two forms — pick one based on which ID type you have:

| Operation | UUID form | External-ID form |
|---|---|---|
| Get customer | `GET /v1/customers/{id}` | `GET /v1/customer-by-external-id/{external_id}` |
| Update customer | `PATCH /v1/customers/{id}` | `PATCH /v1/customer-by-external-id/{external_id}` |
| Delete customer | `DELETE /v1/customers/{id}` | `DELETE /v1/customer-by-external-id/{external_id}` |
| Get balance | `GET /v1/customers/{id}/credits` | `GET /v1/customer-by-external-id/{external_id}/credits` |
| Manual grant | `POST /v1/customers/{id}/credits/grant` | `POST /v1/customer-by-external-id/{external_id}/credits/grant` |
| Adjust credits | `POST /v1/customers/{id}/credits/adjust` | `POST /v1/customer-by-external-id/{external_id}/credits/adjust` |
| Ledger history | `GET /v1/customers/{id}/credits/history` | `GET /v1/customer-by-external-id/{external_id}/credits/history` |
| Entitlement check | `GET /v1/customers/{id}/entitlements/{key}` | `GET /v1/customer-by-external-id/{external_id}/entitlements/{key}` |
| List topups | `GET /v1/customers/{id}/topups` | `GET /v1/customer-by-external-id/{external_id}/topups` |

The external-id family uses **singular** `customer-by-external-id` (no `s`). Don't confuse this with the plural `/customers/` path.

## URL-encoding external IDs

If your `external_id` contains special characters (`:`, `/`, spaces), URL-encode the entire segment:

| Character | Encoded |
|---|---|
| `:` | `%3A` |
| `/` | `%2F` |
| ` ` | `%20` |

Example for a compound ID like `userId:companionId`:

```
PATCH /v1/customer-by-external-id/userId%3AcompanionId
{
  "display_name": "Siddharth × Kabir"
}
```

## Auto-creation

When you reference a customer that QuotaStack hasn't seen before, certain write endpoints auto-create the customer record on the fly. Read endpoints return `404` for unknown customers.

**Auto-create:**

- `POST /v1/usage`
- `POST /v1/usage/batch`
- `POST /v1/reserve`
- `POST /v1/topup/grant`
- `POST /v1/customers/{id}/credits/grant`
- `POST /v1/customer-by-external-id/{ext_id}/credits/grant`

**Return 404 if not found (no auto-create):**

- All `GET` endpoints (`/credits`, `/credits/history`, `/entitlements/{metric}`, `/topups`)
- `PATCH` / `DELETE` on the customer resource
- All reads under `/v1/customer-by-external-id/...`

Auto-created customers start with `display_name: null` — fill it in via `PATCH` when you have a meaningful label. To create a customer explicitly (without a side-effect write), use `POST /v1/customers`.

## The display_name field

Every customer can carry an optional `display_name` — a tenant-defined human label (max 200 chars). QuotaStack uses it for friendly display and search in the admin dashboard. Auto-created customers start with `display_name: null` — the dashboard falls back to rendering the external ID until you set it.

Backfill existing customers via a single `PATCH`:

```bash
curl -X PATCH https://api.quotastack.io/v1/customer-by-external-id/user_abc \
  -H "X-API-Key: qs_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "Alice Nakamura"
  }'
```

`display_name` is not used in billing logic — it's purely a label for humans viewing the dashboard.

## Which form to pick

**Use `external_customer_id`** (recommended for most code paths) when:

- You're writing your own business logic and already have your user's identifier
- You don't want to store QuotaStack's UUID in your database
- You want API calls to read naturally in your code (`user_abc` is more meaningful than an opaque UUID)

**Use `customer_id`** (the UUID) when:

- Receiving webhooks — payloads echo the QuotaStack UUID as the canonical ID
- You've already fetched a customer and have the UUID in hand from a prior response
- You're integrating with QuotaStack-admin tooling that returns UUIDs

## Deleting a customer

`DELETE /v1/customers/{id}` (and the external-id form) marks the customer as deleted for listing and dashboard purposes only. It is a **soft delete** — the record is retained with a `deleted_at` timestamp so history stays intact.

Important: DELETE does **not** cascade. After deletion:

- **Credit accounts** stay. Existing balances are not zeroed.
- **Active reservations** stay. They run until TTL or until you commit/release them.
- **Active subscriptions** stay. Scheduled renewal grants still post credits into the soft-deleted customer's account.
- **Ledger history** is retained in full.

To fully wind down a customer, cancel their subscription (`cancel_immediately: true`), release any outstanding reservations, and void remaining credits via `POST /v1/customers/{id}/credits/adjust` with a negative delta equal to the balance. Then call DELETE to hide the customer from list views. Future releases may add cascade semantics — don't rely on DELETE alone for compliance purposes today.

## Errors

| Status | Cause | Fix |
|---|---|---|
| `400` | Both `customer_id` and `external_customer_id` provided in a body | Remove one |
| `400` | Neither provided | Add one |
| `404` | External ID does not match any customer | Verify the ID, or use an auto-create endpoint (`/usage`, `/topup/grant` auto-create) |
| `422` | Missing required header (e.g. `Idempotency-Key`) | Add the header |

## Related concepts

- [Credits](/docs/concepts/credits) — the balance records keyed by customer
- [Topups and Wallets](/docs/concepts/topups-and-wallets) — where `external_customer_id` is most commonly used
- [Entitlements](/docs/concepts/entitlements) — checks scoped to a customer
- [Idempotency](/docs/concepts/idempotency) — deterministic keys usually include the external ID

## Common Mistakes

**✗ Don't send both `customer_id` and `external_customer_id` in one request body**

QuotaStack returns 400. Pick the one that matches what you have. Your code should choose based on what identifier is at hand, not include both defensively.

**✗ Don't forget to URL-encode `external_id` in URL paths**

If your external ID contains `:`, `/`, or spaces, the URL will misparse. `userId:companionId` must become `userId%3AcompanionId`.

**✗ Don't store QuotaStack UUIDs in your database unless you need to**

Most apps only need their own IDs. Storing the QuotaStack UUID adds a sync burden and a migration risk. The `external_customer_id` path is a first-class citizen.
