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

# Cardholders

> Create and manage cardholders for your card program

Cardholders are the end users who receive and use cards. Each cardholder belongs to a single partner and must pass KYC before cards can be issued.

## KYC methods

Contro supports two ways to submit KYC when creating a cardholder. Choose the method that best fits your integration:

| Method | `kycSource` | Best for |
| - | - | - |
| [KYC via web link](/partner/kyc-via-web-link) | `"web"` (default) | Partners who prefer a hosted KYC flow without handling sensitive data |
| [KYC via Sumsub share token](/partner/kyc-via-sumsub) | `"sumsub"` | Partners already using Sumsub who want to reuse verified KYC |

## Create a cardholder

Creating a cardholder and submitting KYC happen in a single request. The example below uses the web link method (`kycSource: "web"`), where KYC is completed via a hosted session. See the individual KYC method pages for other approaches.

First, create a KYC session via [`POST /partner/kyc-sessions`](/partner/kyc-via-web-link) and wait for the user to complete it, then create the cardholder:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.contro.me/v1/partner/cardholders \
    -H "x-contro-api-key: $CONTRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "externalUserId": "user_42",
      "kycSource": "web",
      "kycSessionId": "ks_abc123",
      "email": "jane@example.com",
      "phoneNumber": "+14155552671"
    }'
  ```

  ```typescript SDK (Node) theme={null}
  const cardholder = await contro.cardholders.create({
    externalUserId: "user_42",
    kycSource: "web",
    kycSessionId: session.id,
    email: "jane@example.com",
    phoneNumber: "+14155552671",
  });
  ```

  ```python SDK (Python) theme={null}
  cardholder = contro.cardholders.create(
      external_user_id="user_42",
      kyc_source="web",
      kyc_session_id=session.id,
      email="jane@example.com",
      phone_number="+14155552671",
  )
  ```

  ```typescript TypeScript (fetch) theme={null}
  const cardholder = await fetch(`${BASE_URL}/partner/cardholders`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      externalUserId: "user_42",
      kycSource: "web",
      kycSessionId: session.id,
      email: "jane@example.com",
      phoneNumber: "+14155552671",
    }),
  }).then((r) => r.json());
  ```
</CodeGroup>

### Required fields

| Field | Type | Description |
| - | - | - |
| `externalUserId` | string | Your unique identifier for this user. Min 1 character. Example: `"user_42"` |
| `email` | string | Valid email address. Example: `"jane@example.com"` |
| `phoneNumber` | string | E.164 formatted phone number. Example: `"+14155552671"` |

### Optional fields

| Field | Type | Description |
| - | - | - |
| `kycSource` | string | KYC method. One of `"web"` (default), `"sumsub"`. See [KYC methods](#kyc-methods) |
| `residenceAddressDetail` | string | Street address. Example: `"531 E Lincoln Ave"` |
| `residenceCity` | string | City. Example: `"Mount Vernon"` |
| `residenceStateProvince` | string | State or province. Example: `"NY"` |
| `residenceCountryCode` | string | Residence country, ISO 3166-1 alpha-2. Example: `"US"` |
| `postalCode` | string | Postal code. Example: `"10552"` |

<Note>
  The residence address fields apply to share token methods (`kycSource: "sumsub"`). Contro reads the address from the shared verification when the document carries one, and falls back to these fields when it does not — a passport, for example, carries no address. Send all five together; a partial address is ignored. See [Residence address](#residence-address).
</Note>

## Residence address

Card issuance requires a complete residence address: street, city, state or province, country, and postal code.

Where that address comes from depends on the KYC method:

| Method | Address source |
| - | - |
| `web` | Collected during the hosted KYC session |
| `sumsub` | Read from the shared verification, falling back to the address you send |

For share token methods, the address extracted from the verified document always takes precedence over the one you send. Send the fields when your user verified with a document that carries no address — a passport is the common case. Without them, the cardholder is created, but card issuance later fails because the address is missing.

You can supply the address when creating the cardholder, or add it later with [`PATCH /partner/cardholders/{id}`](#update-a-cardholder).

## Initiate KYC for a card program

After creating the cardholder, initiate KYC verification for a specific card program. Cards cannot be issued until KYC is approved for that program.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.contro.me/v1/partner/cardholders/{id}/kyc \
    -H "x-contro-api-key: $CONTRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "cardProgramId": "cp_xyz789"
    }'
  ```

  ```typescript SDK (Node) theme={null}
  await contro.cardholders.initiateKyc("ch_abc123", {
    cardProgramId: "cp_xyz789",
  });
  ```

  ```python SDK (Python) theme={null}
  contro.cardholders.initiate_kyc("ch_abc123", card_program_id="cp_xyz789")
  ```
</CodeGroup>

Response:

```json theme={null}
{
  "status": "pending",
  "cardholderId": "ch_abc123",
  "cardProgramId": "cp_xyz789"
}
```

## KYC status

### Check KYC status

The `cardProgramId` query parameter is required.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.contro.me/v1/partner/cardholders/{id}/kyc?cardProgramId=cp_xyz789" \
    -H "x-contro-api-key: $CONTRO_API_KEY"
  ```

  ```typescript SDK (Node) theme={null}
  const kyc = await contro.cardholders.getKyc("ch_abc123", {
    cardProgramId: "cp_xyz789",
  });
  ```

  ```python SDK (Python) theme={null}
  kyc = contro.cardholders.get_kyc("ch_abc123", card_program_id="cp_xyz789")
  ```
</CodeGroup>

Response:

```json theme={null}
{
  "kycStatus": "approved",
  "cardProgramId": "cp_xyz789"
}
```

### KYC status lifecycle

| Status | Description |
| - | - |
| `pending` | Verification in progress |
| `approved` | Verification passed — cards can be issued |
| `rejected` | Verification failed — review and retry |

<Note>
  In sandbox mode, KYC is auto-approved for testing. In production, verification is performed by the KYC provider.
</Note>

## Update a cardholder

Update mutable fields on an existing cardholder:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X PATCH https://api.contro.me/v1/partner/cardholders/{id} \
    -H "x-contro-api-key: $CONTRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "email": "jane.doe@newdomain.com",
      "phoneNumber": "+14155559999",
      "residenceAddressDetail": "531 E Lincoln Ave",
      "residenceCity": "Mount Vernon",
      "residenceStateProvince": "NY",
      "residenceCountryCode": "US",
      "postalCode": "10552"
    }'
  ```

  ```typescript SDK (Node) theme={null}
  await contro.cardholders.update("ch_abc123", {
    email: "jane.doe@newdomain.com",
    phoneNumber: "+14155559999",
    residenceAddressDetail: "531 E Lincoln Ave",
    residenceCity: "Mount Vernon",
    residenceStateProvince: "NY",
    residenceCountryCode: "US",
    postalCode: "10552",
  });
  ```

  ```python SDK (Python) theme={null}
  contro.cardholders.update("ch_abc123",
      email="jane.doe@newdomain.com",
      phone_number="+14155559999",
      residence_address_detail="531 E Lincoln Ave",
      residence_city="Mount Vernon",
      residence_state_province="NY",
      residence_country_code="US",
      postal_code="10552",
  )
  ```
</CodeGroup>

Updatable fields: `email`, `phoneNumber`, `residenceAddressDetail`, `residenceCity`, `residenceStateProvince`, `residenceCountryCode`, `postalCode`.

Address fields are merged into the cardholder — sending only the address leaves `email` and `phoneNumber` unchanged. Use this to add a missing address when card issuance failed for one, then retry issuing the card.

## List cardholders

Retrieve cardholders with optional filtering:

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.contro.me/v1/partner/cardholders?limit=20&status=active&kycStatus=approved" \
    -H "x-contro-api-key: $CONTRO_API_KEY"
  ```

  ```typescript SDK (Node) theme={null}
  const cardholders = await contro.cardholders.list({
    limit: 20,
    status: "active",
    kycStatus: "approved",
  });
  ```

  ```python SDK (Python) theme={null}
  cardholders = contro.cardholders.list(limit=20, status="active", kyc_status="approved")
  ```
</CodeGroup>

### Query parameters

| Parameter | Type | Description |
| - | - | - |
| `page` | integer | Page number (default `1`). |
| `limit` | integer | Items per page (1–100, default `20`). Example: `50` |
| `status` | string | Filter by cardholder status. One of `active`, `suspended`, `closed`. |
| `kycStatus` | string | Filter by KYC status. One of `pending`, `approved`, `rejected`. |
| `externalUserId` | string | Filter by your `externalUserId`. |
| `from` | string | ISO 8601 start timestamp (inclusive) on `createdAt`. |
| `to` | string | ISO 8601 end timestamp (inclusive) on `createdAt`. |

## Get a cardholder

Retrieve a single cardholder by ID:

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.contro.me/v1/partner/cardholders/{id} \
    -H "x-contro-api-key: $CONTRO_API_KEY"
  ```

  ```typescript SDK (Node) theme={null}
  const cardholder = await contro.cardholders.retrieve("ch_abc123");
  ```

  ```python SDK (Python) theme={null}
  cardholder = contro.cardholders.retrieve("ch_abc123")
  ```
</CodeGroup>

### Response fields

| Field | Type | Description |
| - | - | - |
| `id` | string | Contro cardholder ID. Example: `"ch_abc123"` |
| `externalUserId` | string | Your user identifier. Example: `"user_42"` |
| `firstName` | string | First name. Example: `"Jane"` |
| `lastName` | string | Last name. Example: `"Doe"` |
| `email` | string | Email address. Example: `"jane@example.com"` |
| `phoneNumber` | string \| null | E.164 phone number, or `null` if not provided. Example: `"+14155552671"` |
| `kycSource` | string | KYC submission method. One of `sumsub`, `web` |
| `kycStatus` | string | KYC verification status. One of `pending`, `approved`, `rejected` |
| `status` | string | Cardholder account status. One of `active`, `suspended`, `closed` |
| `createdAt` | string | ISO 8601 creation timestamp. Example: `"2026-03-20T14:30:00Z"` |
