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

# Enrollment and signup

> Sign a customer up at the counter: create a pending-proof wallet, verify the phone by SMS, release locked promo, and resolve claims.

## Overview

Enrollment turns a phone number the cashier typed into a verified Feddi customer who can spend, in two calls. `POST /enroll/initiate` creates a wallet identity in the `pending_proof` state and sends an SMS verification link. `POST /enroll/verify` proves phone ownership and atomically resolves every deferred side-effect, including releasing locked promotional grants.

The single rule that governs this page: a `LOCKED` promotional grant releases only when the customer reaches `verified` and the claim-gate is satisfied. Proof of phone is the one deliberate exception in the money model.

A customer's balance is scoped per merchant. The same phone number at a different merchant is a different customer with a different balance.

Read [PENDING\_PROOF and promo release](/concepts/pending-proof) before you wire any cashback or signup path. This page assumes you know it.

## Operations

Four operations make up the callable signup surface. All are stable and callable today.

<ParamField query="POST /enroll/initiate" param-type="endpoint">
  Create the `pending_proof` wallet identity and send the verification SMS.
</ParamField>

<ParamField query="POST /enroll/verify" param-type="endpoint">
  Prove phone ownership, flip to `verified`, and resolve side-effects atomically.
</ParamField>

<ParamField query="POST /enroll/resend" param-type="endpoint">
  Resend the verification SMS. Rate-limited and attempt-capped.
</ParamField>

<ParamField query="POST /claims" param-type="endpoint">
  Drive the claim release-engine directly. Used for ops re-drive; `verify` runs the same engine inline.
</ParamField>

## The signup flow

A promo grant lands `LOCKED` on the proto-wallet, the customer proves their phone by SMS, and verify releases the held promo in one transaction.

```mermaid theme={null}
sequenceDiagram
    participant POS as POS / Cashier
    participant Feddi as Feddi Partner API
    participant Cust as Customer

    Note over POS: Cashier types the phone
    POS->>Feddi: POST /enroll/initiate
    Feddi-->>POS: customer_state pending_proof, verification_sent
    Feddi--)Cust: SMS verification link

    Note over Cust: Earlier promo escrows LOCKED on the proto-wallet

    Cust->>Feddi: Open SMS link (proof of phone)
    POS->>Feddi: POST /enroll/verify
    Feddi-->>POS: customer_state verified, promo_balance_minor, released_grants
```

`POST /enroll/initiate` find-or-creates a wallet identity in the `pending_proof` state for the merchant and normalized phone, mints a single-use signed verification link (a short-TTL JWT), and sends it by SMS. No money moves at this step and no provider-customer link is created yet. The verification SMS is transactional and needs no marketing consent opt-in.

`POST /enroll/verify` decodes the signed token, guards that it is not expired, not consumed, and that the wallet identity is still `pending_proof`, then in one transaction flips `pending_proof` to `verified` and resolves every deferred side-effect atomically.

<Steps>
  <Step title="Bind the POS customer id">
    Create the provider-customer link so the POS provider's customer id resolves to this wallet identity on every future order.
  </Step>

  <Step title="Bridge to the canonical wallet">
    Find-or-create the canonical wallet for the verified identity so a money ledger exists.
  </Step>

  <Step title="Release the locked promo">
    Run the claim-gate against every `LOCKED` cashback-class grant escrowed on the proto-wallet, flipping eligible grants to `RELEASED` and crediting spendable promo balance.
  </Step>

  <Step title="Arm offers eligibility">
    Mark the customer eligible for marketing and offers, subject to recorded consent.
  </Step>
</Steps>

None of these resolve partially. If any step fails, the whole flip rolls back and the link stays consumable. Domain events publish after the transaction commits, never inside it.

<Info>
  Verify is idempotent. A re-submit of an already-consumed token replays the original verified result with `meta.idempotency_replayed: true`. It does not re-release grants. A genuinely already-consumed token returns HTTP 200 with the replay flag, not a 422.
</Info>

## Initiate the signup

`POST /enroll/initiate` opens the flow. Supply the phone the cashier entered. Pass `feddi_session_id` in `context` to anchor the enrollment to a live checkout session, or omit it for a standalone signup.

This call is idempotent on the `Idempotency-Key` header. A retry with the same key replays the original wallet identity and link without sending a new SMS and without resetting the resend rate-limit window.

<ParamField header="Idempotency-Key" param-type="string" required="true">
  A partner-generated UUID. Required on every mutating call.
</ParamField>

<ParamField body="context" param-type="object" required="true">
  Request context. `merchant_id` is required; `branch_id`, `terminal_id`, `cashier_id`, `partner_session_id`, and `feddi_session_id` are optional. See [Conventions](/api-reference/conventions).
</ParamField>

<ParamField body="phone" param-type="string" required="true">
  The customer phone number. Feddi normalizes it to E.164. An invalid or unsupported country code returns `VALIDATION_ERROR`.
</ParamField>

<ParamField body="provider_customer_id" param-type="string" required="false">
  The POS provider's customer id. Bound to the wallet identity at verify, not here.
</ParamField>

<ParamField body="wallet_program_id" param-type="string" required="false">
  Disambiguate when the phone resolves to more than one program. Omitting it when the phone is ambiguous returns `WALLET_PROGRAM_AMBIGUOUS` with the candidate program ids in `error.details.candidates`.
</ParamField>

<ParamField body="language" param-type="string" required="false">
  Preferred SMS language for the verification link, for example `en` or `ar`. Defaults to the program or branch default.
</ParamField>

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST https://api.feddi.io/v1/partner/enroll/initiate \
    -H "Authorization: Bearer $TERMINAL_JWT" \
    -H "Idempotency-Key: 1a2b3c4d-0001-4f9b-9d1e-1a2b3c4d5e6f" \
    -H "Content-Type: application/json" \
    -d '{
      "meta": { "partner_request_id": "1a2b3c4d-0001-4f9b-9d1e-1a2b3c4d5e6f", "occurred_at": "2026-06-05T09:40:00Z", "sent_at": "2026-06-05T09:40:01Z", "api_version": "2026-06-01" },
      "context": { "merchant_id": "11111111-1111-1111-1111-111111111111", "branch_id": "22222222-2222-2222-2222-222222222222", "terminal_id": "POS-360-0007", "cashier_id": "cashier-42", "partner_session_id": "ord-99841", "feddi_session_id": "33333333-3333-3333-3333-333333333333" },
      "phone": "+97433001122",
      "provider_customer_id": "pos-cust-5521",
      "language": "en"
    }'
  ```

  ```json 200 OK theme={null}
  {
    "ok": true,
    "data": {
      "wallet_user_id": "wu_aa01",
      "customer_state": "pending_proof",
      "phone": "+97433001122",
      "wallet_program_id": "wp_77",
      "verification_sent": true,
      "verification_channel": "sms",
      "verification_expires_at": "2026-06-05T10:10:00Z",
      "provider_customer_map_created": false,
      "is_new": true,
      "session_id": "33333333-3333-3333-3333-333333333333"
    },
    "error": null,
    "meta": { "request_id": "req_en1", "idempotency_replayed": false, "api_version": "2026-06-01" }
  }
  ```

  ```json 422 WALLET_PROGRAM_AMBIGUOUS theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "WALLET_PROGRAM_AMBIGUOUS",
      "message": "Phone resolves to multiple wallet programs; specify wallet_program_id.",
      "details": { "candidates": ["wp_77", "wp_91"] }
    },
    "meta": { "request_id": "req_en9", "idempotency_replayed": false, "api_version": "2026-06-01" }
  }
  ```
</CodeGroup>

<ResponseField name="wallet_user_id" field-type="string" required="true">
  The id of the wallet identity. Persist it to address the same customer on verify, resend, and claims.
</ResponseField>

<ResponseField name="customer_state" field-type="string" required="true">
  `pending_proof` for a new or unverified enrollment. `verified` if the phone was already a verified customer (an idempotent no-op enroll).
</ResponseField>

<ResponseField name="phone" field-type="string">
  The normalized E.164 phone.
</ResponseField>

<ResponseField name="wallet_program_id" field-type="string">
  The resolved program id.
</ResponseField>

<ResponseField name="verification_sent" field-type="boolean" required="true">
  Whether a verification SMS was dispatched. `false` on an idempotent replay that did not re-send.
</ResponseField>

<ResponseField name="verification_channel" field-type="string">
  One of `sms`, `whatsapp`, or `null`.
</ResponseField>

<ResponseField name="verification_expires_at" field-type="string">
  When the verification link expires. After this, the customer needs a resend.
</ResponseField>

<ResponseField name="provider_customer_map_created" field-type="boolean">
  Always `false` at initiate. The provider-customer link is created at verify.
</ResponseField>

<ResponseField name="is_new" field-type="boolean">
  `true` if this call created the proto-wallet, `false` if it already existed.
</ResponseField>

<ResponseField name="session_id" field-type="string">
  The checkout session this enrollment anchored to, when `feddi_session_id` was supplied.
</ResponseField>

## Verify the phone

`POST /enroll/verify` proves phone ownership. The customer clicks the SMS link, or the cashier keys the code. Supply `verification_token` or `code`. This call releases promo, so treat it as a money path. It is idempotent on the `Idempotency-Key` header.

<ParamField header="Idempotency-Key" param-type="string" required="true">
  A partner-generated UUID.
</ParamField>

<ParamField body="context" param-type="object" required="true">
  Request context. `merchant_id` is required.
</ParamField>

<ParamField body="verification_token" param-type="string" required="false">
  The signed single-use token from the SMS link. Supply this or `code`.
</ParamField>

<ParamField body="code" param-type="string" required="false">
  The numeric code the cashier keyed, as an alternative to the link token.
</ParamField>

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST https://api.feddi.io/v1/partner/enroll/verify \
    -H "Authorization: Bearer $TERMINAL_JWT" \
    -H "Idempotency-Key: 2b3c4d5e-0002-4f9b-9d1e-1a2b3c4d5e6f" \
    -H "Content-Type: application/json" \
    -d '{
      "meta": { "partner_request_id": "2b3c4d5e-0002-4f9b-9d1e-1a2b3c4d5e6f", "occurred_at": "2026-06-05T09:45:00Z", "sent_at": "2026-06-05T09:45:01Z", "api_version": "2026-06-01" },
      "context": { "merchant_id": "11111111-1111-1111-1111-111111111111", "branch_id": "22222222-2222-2222-2222-222222222222", "terminal_id": "POS-360-0007" },
      "verification_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.proof.signature"
    }'
  ```

  ```json 200 OK theme={null}
  {
    "ok": true,
    "data": {
      "wallet_user_id": "wu_aa01",
      "customer_state": "verified",
      "verified_at": "2026-06-05T09:45:02Z",
      "wallet_id": "wal_5512",
      "wallet_program_id": "wp_77",
      "provider_customer_map_created": true,
      "balance_minor": 5000,
      "promo_balance_minor": 250,
      "currency": "QAR",
      "released_grants": [
        { "promo_grant_id": "pg_bb02", "released_minor": 250, "source": "SKU_TOPUP_BONUS" }
      ]
    },
    "error": null,
    "meta": { "request_id": "req_ev1", "idempotency_replayed": false, "api_version": "2026-06-01" }
  }
  ```

  ```json 422 Expired token theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "VALIDATION_ERROR",
      "message": "Verification token expired. Resend a fresh link.",
      "details": { "reason": "token_expired" }
    },
    "meta": { "request_id": "req_ev9", "idempotency_replayed": false, "api_version": "2026-06-01" }
  }
  ```
</CodeGroup>

<ResponseField name="wallet_user_id" field-type="string" required="true">
  The id of the now-verified wallet identity.
</ResponseField>

<ResponseField name="customer_state" field-type="string" required="true">
  Always `verified` on success or an idempotent replay of a prior verify.
</ResponseField>

<ResponseField name="verified_at" field-type="string">
  When verification completed.
</ResponseField>

<ResponseField name="wallet_id" field-type="string">
  The canonical wallet bridged at verify.
</ResponseField>

<ResponseField name="wallet_program_id" field-type="string">
  The resolved program id.
</ResponseField>

<ResponseField name="provider_customer_map_created" field-type="boolean">
  Whether a new provider-customer link was created. `false` on a replay.
</ResponseField>

<ResponseField name="balance_minor" field-type="integer">
  The verified customer's actual (real-money) balance in minor units. Pair with `currency`.
</ResponseField>

<ResponseField name="promo_balance_minor" field-type="integer">
  Released, spendable promotional balance in minor units, after the claim-gate ran.
</ResponseField>

<ResponseField name="currency" field-type="string">
  ISO-4217 currency for the balance fields.
</ResponseField>

<ResponseField name="released_grants" field-type="array">
  The grants this verify released, empty if none were escrowed. Each carries `promo_grant_id`, `released_minor`, and `source` (one of `CASHBACK`, `RELOAD_BONUS`, `SKU_TOPUP_BONUS`, `GATEWAY_BONUS`, `SIGNUP_BONUS`).
</ResponseField>

<Info>
  Money is always minor units with an explicit currency. `balance_minor: 5000` with `currency: 'QAR'` means 50.00 QAR. The server is authoritative. Never compute money client-side.
</Info>

<Expandable title="Handle an expired or invalid token">
  Verify returns `VALIDATION_ERROR` (HTTP 422) for an expired token, or for a wallet identity that already left `pending_proof` in a non-idempotent way. Call `POST /enroll/resend` to mint a fresh single-use token. The prior token is invalidated. A token that does not resolve to a `pending_proof` identity for your integration returns `NOT_FOUND` (HTTP 404). A token belonging to another integration returns `FORBIDDEN` (HTTP 403).
</Expandable>

## Resend the verification SMS

`POST /enroll/resend` mints a fresh single-use token and invalidates the prior one. Resolve the target by `wallet_user_id` or `phone`. The target must be in `pending_proof`; a verified customer needs no link and returns `VALIDATION_ERROR`. This is a transactional message and needs no consent gate.

Send limits protect the customer from SMS bombing: a maximum of 3 sends per rolling 24-hour window, with a minimum interval of 60 seconds between sends. A send that exceeds the cap is rejected with `RATE_LIMITED` (HTTP 429) and `error.details.retry_after_seconds`. A retry within the 60-second floor replays the prior dispatch rather than counting as a new send.

<ParamField header="Idempotency-Key" param-type="string" required="true">
  A partner-generated UUID.
</ParamField>

<ParamField body="context" param-type="object" required="true">
  Request context. `merchant_id` is required.
</ParamField>

<ParamField body="wallet_user_id" param-type="string" required="false">
  The `pending_proof` wallet identity to resend to. Supply this or `phone`.
</ParamField>

<ParamField body="phone" param-type="string" required="false">
  E.164 phone, normalized identically to initiate. An alternative to `wallet_user_id`.
</ParamField>

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST https://api.feddi.io/v1/partner/enroll/resend \
    -H "Authorization: Bearer $TERMINAL_JWT" \
    -H "Idempotency-Key: 3c4d5e6f-0003-4f9b-9d1e-1a2b3c4d5e6f" \
    -H "Content-Type: application/json" \
    -d '{
      "meta": { "partner_request_id": "3c4d5e6f-0003-4f9b-9d1e-1a2b3c4d5e6f", "occurred_at": "2026-06-05T09:50:00Z", "sent_at": "2026-06-05T09:50:00Z", "api_version": "2026-06-01" },
      "context": { "merchant_id": "11111111-1111-1111-1111-111111111111", "branch_id": "22222222-2222-2222-2222-222222222222", "terminal_id": "POS-360-0007" },
      "wallet_user_id": "wu_aa01"
    }'
  ```

  ```json 200 OK theme={null}
  {
    "ok": true,
    "data": {
      "wallet_user_id": "wu_aa01",
      "verification_sent": true,
      "verification_expires_at": "2026-06-05T10:20:00Z",
      "sends_used_24h": 2,
      "sends_remaining_24h": 1,
      "next_send_allowed_at": "2026-06-05T09:51:00Z"
    },
    "error": null,
    "meta": { "request_id": "req_rs1", "idempotency_replayed": false, "api_version": "2026-06-01" }
  }
  ```

  ```json 429 RATE_LIMITED theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "RATE_LIMITED",
      "message": "Verification SMS resend rate limit reached. Try again later.",
      "details": { "retry_after_seconds": 41, "sends_used_24h": 3, "cap_24h": 3 }
    },
    "meta": { "request_id": "req_rs9", "idempotency_replayed": false, "api_version": "2026-06-01" }
  }
  ```
</CodeGroup>

<ResponseField name="wallet_user_id" field-type="string" required="true">
  The wallet identity the SMS was sent to.
</ResponseField>

<ResponseField name="verification_sent" field-type="boolean" required="true">
  Whether the SMS was dispatched.
</ResponseField>

<ResponseField name="verification_expires_at" field-type="string">
  When the new verification link expires.
</ResponseField>

<ResponseField name="sends_used_24h" field-type="integer">
  Sends used in the rolling 24-hour window, including the initiate send.
</ResponseField>

<ResponseField name="sends_remaining_24h" field-type="integer">
  Remaining sends before the 3-per-24h cap.
</ResponseField>

<ResponseField name="next_send_allowed_at" field-type="string">
  The earliest time the 60-second floor allows another send.
</ResponseField>

## Drive claims directly

`POST /claims` is the explicit, idempotent driver for the claim release-engine: it converts a verified customer's `LOCKED` cashback-class grants into spendable promo balance. The common signup path does not need it, because `POST /enroll/verify` invokes the same engine inline. Reach for `POST /claims` for ops re-drive, and for the top-up-before-register ordering where verification already happened but a later accrual still needs releasing.

For the top-up-before-register ordering, verify already ran with nothing to release, a later accrual escrows a fresh `LOCKED` grant, and `POST /claims` releases it.

```mermaid theme={null}
sequenceDiagram
    participant POS as POS / Cashier
    participant Feddi as Feddi Partner API

    POS->>Feddi: POST /enroll/verify
    Feddi-->>POS: customer_state verified, released_grants empty

    Note over Feddi: Later accrual escrows a LOCKED grant

    POS->>Feddi: POST /claims
    Feddi-->>POS: state released, released_minor, promo_balance_after_minor
```

The engine converges across event orderings. Register-before-accrual and accrual-before-register both converge to the same released balance. A claim after clawback releases `0`. A duplicate claim is an idempotent no-op replay. Concurrent claims collapse to exactly one creditor.

Resolve the customer by `wallet_user_id` or an identity credential in `customer`. At least one is required. The call is idempotent on the `Idempotency-Key` header.

<Warning>
  The claim-gate requires `verified`. A `LOCKED` grant is never released by an anonymous-to-identity merge alone; release stays gated on proof of phone. A claim against an unverified customer is denied with `FORBIDDEN` (HTTP 403). A claim that finds nothing claimable returns `state: released` with `released_minor: 0`.
</Warning>

<ParamField header="Idempotency-Key" param-type="string" required="true">
  A partner-generated UUID.
</ParamField>

<ParamField body="context" param-type="object" required="true">
  Request context. `merchant_id` is required.
</ParamField>

<ParamField body="wallet_user_id" param-type="string" required="false">
  The verified wallet identity whose `LOCKED` grants to release. Supply this or `customer`.
</ParamField>

<ParamField body="customer" param-type="object" required="false">
  An identity credential resolving the customer, as an alternative to `wallet_user_id`.
</ParamField>

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST https://api.feddi.io/v1/partner/claims \
    -H "Authorization: Bearer $TERMINAL_JWT" \
    -H "Idempotency-Key: 4d5e6f70-0004-4f9b-9d1e-1a2b3c4d5e6f" \
    -H "Content-Type: application/json" \
    -d '{
      "meta": { "partner_request_id": "4d5e6f70-0004-4f9b-9d1e-1a2b3c4d5e6f", "occurred_at": "2026-06-05T09:55:00Z", "sent_at": "2026-06-05T09:55:00Z", "api_version": "2026-06-01" },
      "context": { "merchant_id": "11111111-1111-1111-1111-111111111111", "branch_id": "22222222-2222-2222-2222-222222222222" },
      "wallet_user_id": "wu_aa01"
    }'
  ```

  ```json 200 OK theme={null}
  {
    "ok": true,
    "data": {
      "claim_id": "clm_01",
      "wallet_user_id": "wu_aa01",
      "state": "released",
      "customer_state": "verified",
      "wallet_id": "wal_5512",
      "released_minor": 250,
      "promo_balance_after_minor": 250,
      "currency": "QAR",
      "released_grants": [
        { "promo_grant_id": "pg_bb02", "released_minor": 250, "source": "SKU_TOPUP_BONUS", "state": "RELEASED", "expires_at": "2026-09-05T00:00:00Z" }
      ],
      "skipped_grants": []
    },
    "error": null,
    "meta": { "request_id": "req_cl1", "idempotency_replayed": false, "api_version": "2026-06-01", "decision_trace_id": "dt_claim_7af2" }
  }
  ```

  ```json 403 FORBIDDEN theme={null}
  {
    "ok": false,
    "data": null,
    "error": {
      "code": "FORBIDDEN",
      "message": "Customer is not verified; the claim-gate denies release.",
      "details": { "customer_state": "pending_proof" }
    },
    "meta": { "request_id": "req_cl9", "idempotency_replayed": false, "api_version": "2026-06-01" }
  }
  ```
</CodeGroup>

<ResponseField name="claim_id" field-type="string" required="true">
  The id of the claim resource.
</ResponseField>

<ResponseField name="wallet_user_id" field-type="string" required="true">
  The resolved wallet identity.
</ResponseField>

<ResponseField name="state" field-type="string" required="true">
  The claim-state-machine value. One of `pending` (awaiting verify or gate), `released` (grants credited), `expired` (grants lapsed before claim), or `clawed_back` (grants reversed before claim, releases `0`).
</ResponseField>

<ResponseField name="customer_state" field-type="string">
  One of `verified`, `pending_proof`, or `null`.
</ResponseField>

<ResponseField name="wallet_id" field-type="string">
  The canonical wallet credited.
</ResponseField>

<ResponseField name="released_minor" field-type="integer">
  Total promo released by this claim in minor units. `0` when nothing was claimable.
</ResponseField>

<ResponseField name="promo_balance_after_minor" field-type="integer">
  The spendable promo balance after the claim, in minor units.
</ResponseField>

<ResponseField name="currency" field-type="string">
  ISO-4217 currency for the money fields.
</ResponseField>

<ResponseField name="released_grants" field-type="array">
  The grants released by this claim. Each carries `promo_grant_id`, `released_minor`, `source`, `state`, and `expires_at`.
</ResponseField>

<ResponseField name="skipped_grants" field-type="array">
  Grants not released, each with `promo_grant_id` and a `reason`. The reason is one of `already_clawed_back`, `expired`, `gate_not_met`, or `already_released`.
</ResponseField>

<Info>
  `meta.decision_trace_id` is an opaque correlation id. Persist it and echo it where a call accepts one.
</Info>

## Errors

Every error is a typed string code in `error.code`, never a bare HTTP status number. Branch on `error.code`, not the HTTP status. See [Idempotency and errors](/concepts/idempotency-and-errors) and the [Error reference](/api-reference/errors).

<ResponseField name="VALIDATION_ERROR" field-type="HTTP 400 or 422">
  Malformed body, invalid or unsupported phone, expired token, or a wallet identity not in the required state. `details` carries the field errors.
</ResponseField>

<ResponseField name="INVALID_API_KEY" field-type="HTTP 401">
  Missing or invalid `x-api-key` or terminal JWT (expired, malformed, or wrong audience).
</ResponseField>

<ResponseField name="FORBIDDEN" field-type="HTTP 403">
  Authorized key but wrong merchant or branch scope, a token belonging to another integration, or a claim against an unverified customer (the claim-gate denies).
</ResponseField>

<ResponseField name="NOT_FOUND" field-type="HTTP 404">
  Unknown customer, or a customer in another tenant. Cross-tenant requests resolve to `NOT_FOUND`, never a leak.
</ResponseField>

<ResponseField name="WALLET_PROGRAM_AMBIGUOUS" field-type="HTTP 422">
  The phone resolves to multiple programs and none was specified. Candidates are listed in `error.details.candidates`.
</ResponseField>

<ResponseField name="IDEMPOTENCY_KEY_REUSED" field-type="HTTP 422">
  Same `Idempotency-Key`, different payload.
</ResponseField>

<ResponseField name="RATE_LIMITED" field-type="HTTP 429">
  Resend exceeded the 60-second floor or the 3-per-24h cap. `error.details.retry_after_seconds` tells you when to retry.
</ResponseField>

## Where to go next

<Columns cols="2">
  <Card title="PENDING_PROOF and promo release" href="/concepts/pending-proof" icon="lock">
    Why locked promo releases only on verified plus the claim-gate, and how grants escrow.
  </Card>

  <Card title="Money: actual vs promotional" href="/concepts/money-classes" icon="coins">
    The two-class balance model that enrollment unlocks.
  </Card>

  <Card title="Top-up and reload bonus" href="/guides/topup" icon="wallet">
    Where the locked promo grants that signup releases come from.
  </Card>

  <Card title="Identity and credentials" href="/concepts/identity-and-credentials" icon="fingerprint">
    How a customer is resolved before you enroll them.
  </Card>
</Columns>
