> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brand-wallet.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create a customer

> Create a bare BrandWallet customer without enrolling them into a campaign.

```http theme={null}
POST $BASE_URL/v1/customers
```

**Creates a bare customer — no card, barcode, or loyalty balances are created. To attach the customer to a campaign, use [POST /v1/customers/register](/api/register-customer).**

> **Auth:** `Authorization: Bearer <accessToken>` — an organization-scoped token from [Authentication](/api/authentication). Missing or invalid token returns **401**; an organization without an active subscription returns `invalid_subscription` (**403**).

Payload schema:

```json theme={null}
{
  "data": {                             // required — free-form customer profile
    "phone": "+905551234567",
    "email": "jane@example.com",
    "fullname": "Jane Doe"
  },
  "metaData": { "source": "pos" },      // optional
  "consents": { "marketing": true | false },  // optional
  "opaqueId": "ext-cust-10432"          // optional — your own customer id
}
```

### Body

| Field | Type | Description |
| - | - | - |
| `data` | object | Required. Free-form customer profile; stored as-is. `phone` and `email` are validated when present. |
| `data.phone` | string | Optional. Normalized to E.164 (`0555 123 45 67` → `+905551234567`); numbers without a country code default to **TR**, so send non-Turkish numbers with a leading `+`. Unparseable numbers return `invalid_phone` (400). Unique within the organization — `existing_customer_with_phone` (400) on conflict. |
| `data.email` | string | Optional. Unique within the organization — `existing_customer_with_email` (400) on conflict. |
| `metaData` | object | Optional. Free-form metadata; stored as-is. |
| `consents` | object | Optional. Consent flags; stored as-is. |
| `opaqueId` | string | Optional. Your own system's customer id. Unique within the organization — `opaque_id_already_exists` (400) on conflict. |

> Uniqueness checks ignore deleted customers. When `email` or `phone` hits an existing customer, look the customer up with [GET /v1/customers/find](/api/find-customer) and pass their id to [POST /v1/customers/register](/api/register-customer) as `customer.id`.

### Example

```bash theme={null}
curl --request POST \
  --url "$BASE_URL/v1/customers" \
  --header 'Authorization: Bearer <accessToken>' \
  --header 'Content-Type: application/json' \
  --data '{
    "data": { "phone": "+905551234567", "fullname": "Jane Doe" },
    "opaqueId": "ext-cust-10432"
  }'
```

### Response

Returns **201** with the bare customer — `cards` is always empty and `collectables` is not present.

```json theme={null}
{
  "id": "9b2f6c1e-3d4a-4f8b-9c0d-7e5a1b2c3d4e",
  "data": {
    "phone": "+905551234567",
    "fullname": "Jane Doe"
  },
  "opaqueId": "ext-cust-10432",
  "status": 1,
  "createdAt": "2026-09-29T08:22:52.795Z",
  "updatedAt": "2026-09-29T08:22:52.795Z",
  "cards": []
}
```

Notes:

* Optional fields that have no value are omitted from the response — you will not receive `null`. Exception: `opaqueId` is `null` when not assigned.
* `data.phone` is always returned in its normalized (E.164) form — the raw format you sent is not stored.
* A bare customer cannot take part in transactions (they have no card). Register them into a campaign first — the card, barcode, and loyalty balances are created at that step.
