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

# Email verification

> Verify an individual account's email with a Stableyard code, or assert an email you already verified.

An individual's email is the first thing Stableyard verifies, and identity verification cannot start without it. A `business` account does not use this step.

## Email policy

Your app environment uses exactly one policy, reported at `capabilities.accounts.emailVerification` in `GET /v2/partners/config`:

```json theme={null}
"emailVerification": { "mode": "stableyard_email_otp", "partnerAssertionAllowed": false, "version": 0 }
```

| `mode` | What happens |
| - | - |
| `stableyard_email_otp` | You request a challenge. Stableyard emails a six-digit code. Your screen collects it and your backend confirms it |
| `partner_asserted` | You assert an email your platform already verified. Stableyard sends no code. Only where Stableyard has enabled it, shown by `partnerAssertionAllowed: true` |

The policies do not mix. An assertion under `stableyard_email_otp` is refused with `403`, and a code request under `partner_asserted` is refused with `400`.

Under `stableyard_email_otp` the customer receives two emails from Stableyard that name your app: the code, then a welcome email. Asserting the email sends neither. See [Branding and your frontend](/white-label/branding-and-frontend#surfaces-that-are-not-yours).

## Request a code

```bash theme={null}
curl -X POST https://staging-api-v2.stableyard.fi/v2/accounts/acct_123/email/verification-challenges \
  -u "$APP_ID:$APP_SECRET" \
  -H "Idempotency-Key: email:acct_123:v1" \
  -H "Content-Type: application/json" \
  -d '{ "email": "jane@example.com" }'
```

```json theme={null}
{
  "contact": { "id": "account_contact_123", "type": "email", "email": "ja**@example.com", "status": "pending_verification" },
  "challenge": {
    "id": "account_email_123",
    "email": "ja**@example.com",
    "status": "pending",
    "verificationMethod": "stableyard_email_otp",
    "attemptsRemaining": 5,
    "expiresAt": "2026-10-01T12:10:00Z"
  },
  "nextAction": { "type": "enter_email_verification_code" }
}
```

| Field | Required | Notes |
| - | - | - |
| `Idempotency-Key` header | Yes | 1 to 256 printable characters |
| `email` | Yes | Up to 320 characters |
| `verification` | No | Only for an assertion. See [Assert an email you already verified](#assert-an-email-you-already-verified) |

* **The code is single-use and valid for ten minutes.**
* **A new challenge replaces the old one.** Requesting a code expires any pending challenge on the account.
* **A replay sends nothing.** The same `Idempotency-Key` with the same email returns the original challenge without emailing another code. Use a new key for a new code.

## Confirm the code

Send the code your customer typed to the challenge:

```bash theme={null}
curl -X POST https://staging-api-v2.stableyard.fi/v2/accounts/acct_123/email/verification-challenges/account_email_123/confirm \
  -u "$APP_ID:$APP_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "code": "482913" }'
```

```json theme={null}
{
  "contact": {
    "id": "account_contact_123",
    "email": "ja**@example.com",
    "status": "verified",
    "verificationMethod": "stableyard_email_otp",
    "verifiedAt": "2026-10-01T12:03:00Z"
  },
  "challenge": { "id": "account_email_123", "status": "verified", "attemptsRemaining": 5 },
  "nextAction": { "type": "start_kyc_session" }
}
```

A correct code returns `contact.status: "verified"` and `nextAction.type: "start_kyc_session"`, and Stableyard sends the customer a welcome email. Confirming a challenge that is already verified returns the same result.

A wrong code returns `401` and lowers `attemptsRemaining`. After five wrong codes, or ten minutes, the challenge is over: request a new one with a new `Idempotency-Key`.

## Assert an email you already verified

Only under `partner_asserted`:

```bash theme={null}
curl -X POST https://staging-api-v2.stableyard.fi/v2/accounts/acct_123/email/verification-challenges \
  -u "$APP_ID:$APP_SECRET" \
  -H "Idempotency-Key: email:acct_123:v1" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "jane@example.com",
    "verification": { "method": "partner_asserted", "verifiedAt": "2026-09-30T09:12:00Z", "reference": "signup_7781" }
  }'
```

| Field | Required | Notes |
| - | - | - |
| `verification.method` | Yes | Always `partner_asserted` |
| `verification.verifiedAt` | No | When your platform verified the address, RFC 3339. It cannot be in the future. Omit it and Stableyard timestamps acceptance |
| `verification.reference` | No | Your own verification reference, 1 to 128 characters of `A-Z a-z 0-9 _ . : -`. Never returned |

The contact is verified at once with `verificationMethod: "partner_asserted"`, and the response carries `nextAction.type: "start_kyc_session"`.

You can supply the email at creation instead. On `POST /v2/accounts`, `email` alone starts the code flow, and `email` with `emailVerified: true` asserts it. The same policy rules apply.

## Read the email status

```bash theme={null}
curl https://staging-api-v2.stableyard.fi/v2/accounts/acct_123/email \
  -u "$APP_ID:$APP_SECRET"
```

```json theme={null}
{
  "accountId": "acct_123",
  "contact": {
    "id": "account_contact_123",
    "type": "email",
    "email": "ja**@example.com",
    "status": "verified",
    "verifiedAt": "2026-10-01T12:03:00Z",
    "verificationMethod": "stableyard_email_otp"
  }
}
```

`contact` is `null` until an email has been requested or asserted. API responses always mask the address.

A managed Vault's initial policy code verifies this same contact, with `verificationMethod: "managed_policy_email_otp"`. Stableyard keeps no separate email identity for fiat or Vaults.

## Email states

```mermaid theme={null}
stateDiagram-v2
  [*] --> pending: challenge created
  pending --> verified: correct code
  pending --> failed: five wrong codes
  pending --> expired: ten minutes pass
  verified --> [*]
  failed --> [*]
  expired --> [*]
```

| Object | Field | Values |
| - | - | - |
| Contact | `status` | `pending_verification`, `verified`, `disabled` |
| Contact | `verificationMethod` | `stableyard_email_otp`, `managed_policy_email_otp`, `partner_asserted`, `legacy_verified` |
| Challenge | `status` | `pending`, `verified`, `expired`, `failed` |
| Response | `nextAction.type` | `enter_email_verification_code`, `request_new_email_verification`, `start_kyc_session`, `none` |

| `nextAction.type` | Do this |
| - | - |
| `enter_email_verification_code` | Collect the code and confirm it |
| `request_new_email_verification` | The challenge failed or expired. Request a new one with a new `Idempotency-Key` |
| `start_kyc_session` | Start [Individual KYC](/concepts/individual-kyc) |
| `none` | The email was already verified. Nothing to do |

**A verified email is final through the API.** Requesting the same address again is a no-op that returns `nextAction.type: "none"`; a different address is refused with `409`. Corrections go through Stableyard operations.

## Errors

| Code | HTTP | When |
| - | - | - |
| `bad_request` | 400 | A code was requested under `partner_asserted` (`details.nextAction.type: "assert_partner_verified_email"`), `Idempotency-Key` is missing or malformed, or `verification.verifiedAt` is invalid or in the future |
| `unauthorized` | 401 | The code is wrong |
| `forbidden` | 403 | An assertion was sent under `stableyard_email_otp` (`details.disabledReason: "stableyard_email_otp_required"`) |
| `not_found` | 404 | The account or challenge is not in your app environment |
| `conflict` | 409 | A different email after verification (`details.nextAction.type: "contact_support"`), an expired challenge, or a challenge that is no longer pending |
| `idempotency_conflict` | 409 | The same `Idempotency-Key` with a different email or assertion |

## Webhooks

| Event | Fires when |
| - | - |
| `account.updated` | A code is requested, and again when the email is verified by code or assertion. Read `GET /v2/accounts/{accountId}/email` |

## Related

<CardGroup cols={2}>
  <Card title="Individual KYC" icon="user" href="/concepts/individual-kyc">
    The next gate: identity verification on a hosted page.
  </Card>

  <Card title="Onboarding overview" icon="user-check" href="/concepts/onboarding">
    Every gate between an account and a fiat rail.
  </Card>

  <Card title="Branding and your frontend" icon="palette" href="/white-label/branding-and-frontend">
    What the code and welcome emails show your customer.
  </Card>

  <Card title="Idempotency" icon="key" href="/idempotency">
    How replays and changed bodies behave for every keyed call.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.