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

# Create User

> Create a new card user with Persona KYC verification

Creates a new user and initiates KYC verification through Persona. Returns a `verificationUrl` where the user completes identity verification.

This is the v2 flow using Persona for KYC — it requires fewer fields than the legacy v1 endpoint and provides a smoother verification experience.

<ParamField header="Authorization" type="string" required>
  Bearer token. Example: `Bearer sk_staging_your_api_key`
</ParamField>

<ParamField header="idempotency-key" type="string" required>
  Unique key to prevent duplicate user creation. Use a UUID.
</ParamField>

<ParamField body="firstName" type="string" required>
  User's first name. 1-100 characters.
</ParamField>

<ParamField body="lastName" type="string" required>
  User's last name. 1-100 characters.
</ParamField>

<ParamField body="email" type="string" required>
  User's email address. Must be unique across your partner account.
</ParamField>

<ParamField body="phoneNumber" type="string" required>
  International phone number. Example: `+14155555555`
</ParamField>

<ParamField body="accountPurpose" type="string" required>
  Purpose of the account. 1-200 characters. Example: `personal use`, `business expenses`.
</ParamField>

<ParamField body="ipAddress" type="string" required>
  User's IP address at the time of registration.
</ParamField>

<ParamField body="isTermsOfServiceAccepted" type="boolean" required>
  Must be `true`. Confirms the user has accepted the terms of service.
</ParamField>

<ParamField body="program" type="string" required>
  Card program type. Either `custodial` or `non_custodial`.
</ParamField>

<ParamField body="walletAddress" type="string">
  Ethereum wallet address. **Required** if `program` is `non_custodial`.
</ParamField>

<ParamField body="redirectUrl" type="string">
  HTTPS URL to redirect the user after Persona verification. Overrides the default redirect URL set in your [profile](/api-reference/profile/set-redirect-url).
</ParamField>

<ParamField body="address" type="string" required>
  User's street address. 1-500 characters. Example: `123 Main Street`.
</ParamField>

<ParamField body="city" type="string" required>
  User's city. 1-100 characters.
</ParamField>

<ParamField body="region" type="string" required>
  User's state or region. 1-100 characters. Example: `CA`, `Lagos`.
</ParamField>

<ParamField body="postalCode" type="string" required>
  User's postal/zip code. 1-20 characters.
</ParamField>

<ParamField body="countryCode" type="string" required>
  ISO 3166-1 alpha-2 country code of the user's country of residence. Example: `US`, `KE`, `NG`.
</ParamField>

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://services-staging.getplu.com/api/v2/partner/card/users \
    -H "Authorization: Bearer sk_staging_your_api_key" \
    -H "Content-Type: application/json" \
    -H "idempotency-key: 550e8400-e29b-41d4-a716-446655440000" \
    -d '{
      "firstName": "Jane",
      "lastName": "Doe",
      "email": "jane@example.com",
      "phoneNumber": "+14155555555",
      "accountPurpose": "personal use",
      "ipAddress": "203.0.113.42",
      "isTermsOfServiceAccepted": true,
      "program": "custodial",
      "address": "123 Main Street",
      "city": "San Francisco",
      "region": "CA",
      "postalCode": "94105",
      "countryCode": "US"
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    'https://services-staging.getplu.com/api/v2/partner/card/users',
    {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer sk_staging_your_api_key',
        'Content-Type': 'application/json',
        'idempotency-key': '550e8400-e29b-41d4-a716-446655440000',
      },
      body: JSON.stringify({
        firstName: 'Jane',
        lastName: 'Doe',
        email: 'jane@example.com',
        phoneNumber: '+14155555555',
        accountPurpose: 'personal use',
        ipAddress: '203.0.113.42',
        isTermsOfServiceAccepted: true,
        program: 'custodial',
        address: '123 Main Street',
        city: 'San Francisco',
        region: 'CA',
        postalCode: '94105',
        countryCode: 'US',
      }),
    }
  );
  ```
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "status": "success",
    "message": "User created successfully",
    "data": {
      "serviceId": "acme-user-abc123def456ghi",
      "verificationUrl": "https://withpersona.com/verify?inquiry-id=inq_abc123..."
    }
  }
  ```

  ```json 400 — Duplicate user theme={null}
  {
    "status": "error",
    "message": "DUP_USER",
    "data": {
      "error": "User already exists with this email"
    }
  }
  ```

  ```json 402 — Insufficient balance theme={null}
  {
    "status": "error",
    "message": "INSUFFICIENT_WALLET_BALANCE",
    "data": {
      "error": "Partner wallet has insufficient funds for KYC fee"
    }
  }
  ```
</ResponseExample>

## After User Creation

1. Redirect the user to the `verificationUrl` to complete Persona KYC
2. After verification, the user is redirected to your configured redirect URL with `?referenceId={serviceId}`
3. Listen for [KYC webhook events](/webhooks#kyc-events) to know when the user is approved
4. Once approved (`user.kyc.approved`), you can [create a card](/api-reference/cards/create-card) for the user

## Sandbox Testing

In the staging environment, you can control the KYC outcome by appending a keyword to the user's `lastName` field. This lets you test each scenario without going through real identity verification.

| Keyword in `lastName` | KYC Outcome                          | Example `lastName`     |
| --------------------- | ------------------------------------ | ---------------------- |
| `Approved`            | KYC approved — user can create cards | `DoeApproved`          |
| `Denied`              | KYC denied                           | `DoeDenied`            |
| `needsVerification`   | Additional verification required     | `DoeneedsVerification` |

<Info>
  The keyword is case-sensitive and must be appended to the end of the last name with no spaces or separators.
</Info>

### Example Webhook Payloads

**Approved**

```json theme={null}
{
  "event": "user.kyc-event",
  "timestamp": "2026-02-19T20:49:29.927Z",
  "data": {
    "serviceId": "acme-user-7mFQc5aCslAWtkd",
    "status": "approved",
    "email": "jane@example.com",
    "firstName": "Jane",
    "lastName": "DoeApproved"
  }
}
```

**Denied**

```json theme={null}
{
  "event": "user.kyc-event",
  "timestamp": "2026-02-19T21:01:49.217Z",
  "data": {
    "serviceId": "acme-user-yvDaVMdFaug51H4",
    "status": "denied",
    "email": "jane@example.com",
    "firstName": "Jane",
    "lastName": "DoeDenied",
    "reason": "WRONG_USER_REGION, REGULATIONS_VIOLATIONS"
  }
}
```

**Needs Verification**

```json theme={null}
{
  "event": "user.kyc-event",
  "timestamp": "2026-02-19T21:02:49.083Z",
  "data": {
    "serviceId": "acme-user-G9KZgnhgbndDdJg",
    "status": "needsVerification",
    "email": "jane@example.com",
    "firstName": "Jane",
    "lastName": "DoeneedsVerification",
    "verificationLink": "https://kyc.getplu.com/verify?userId=e5508685-2771-48fd-8cbc-c8c3e6df9179"
  }
}
```

<Note>
  `verificationLink` is only present on `needsVerification` events. `reason` is only present on `denied` events and contains one or more comma-separated denial codes.
</Note>
