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

# Send the account holder a code to link the card issuer

> Starts the one-time card issuer link: the card issuer sends a one-time code to the account holder. Then call `POST /verify-card-issuer-link` with the code they enter.

**The account holder, never the agent, receives and enters the code.** Send your human to their own inbox (or phone) and have them type the code into your interface. Never try to read the account holder's email or messages yourself, and never ask for their email password or access.

**Where the code goes.** An account with an email on file always gets the code at that email; `email` and `phone` are ignored for it. An account with no email (a wallet-only sign-in) must send exactly one of `email` or `phone` (E.164). The response names the destination masked, never in full.

**Limits.** Codes last 10 minutes. At most 5 codes can be sent per account in any rolling hour (`429`, with `Retry-After`). Sending a new code replaces the previous one, which then stops working.

**Prerequisite:** Laso identity verification (`403 kyc_required` otherwise, with a `kyc_url`). If the account is already linked, this answers `200` with `status: linked` and sends nothing, so calling it twice is safe.

**This route is free.**



## OpenAPI

````yaml /api-reference/openapi.json post /start-card-issuer-link
openapi: 3.1.0
info:
  title: Laso Finance x402 API
  version: 1.0.0
  x-docs-revision: 3b5f40237119
  x-docs-manifest: https://laso.finance/.well-known/docs-version.json
  contact:
    email: agents+support@laso.finance
  x-guidance: >-
    Laso Finance is a payment-gated (x402) API that lets an AI agent spend USDC
    on real-world financial products: prepaid cards (U.S. and international),
    gift cards, push-to-card transfers to USD/EUR/GBP debit cards, and
    Venmo/PayPal payouts.


    Payment: every paid route is an x402 v2 endpoint. Call it with no payment
    header to receive a 402 challenge listing the accepted networks, then replay
    with a signed USDC payment. Both Base (eip155:8453) and Solana
    (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp) are accepted on every paid route;
    the caller picks either chain.


    Identity: `GET /auth` is free and identity-only. Prove wallet ownership with
    a `SIGN-IN-WITH-X` (CAIP-122) header to receive a Firebase id_token, then
    send that token as a Bearer credential to the authenticated read routes
    (`get-card-data`, `get-account-balance`, `get-kyc-status`, etc.). Paid
    routes also return fresh auth credentials in their response, so a payment is
    never required just to obtain a token.


    Recommended flow: (1) `GET /auth` to establish identity, (2) call a paid
    route (e.g. `GET /get-card`) to purchase a product, paying USDC on Base or
    Solana, (3) poll the authenticated read routes with the returned Bearer
    token to fetch the resulting card/transfer details. Reloadable card setup:
    the account holder links the card issuer once. Check `GET
    /get-card-issuer-status`; if it is `not_started`, call `POST
    /start-card-issuer-link` so the card issuer sends the account holder a
    one-time code, have the HUMAN read it from their own inbox and enter it in
    your interface (never read their email yourself), then `POST
    /verify-card-issuer-link` with the code and `consent_accepted: true`.
    Linking requires Laso identity verification (`kyc_required` returns a
    `kyc_url` for the human). Then fund with `GET /fund-card-balance` and create
    cards with `POST /create-reloadable-card`. Full machine-readable
    instructions live at https://laso.finance/SKILL.md.
  description: >-
    Payment-gated API for Laso Finance. All paywalled routes use the x402
    protocol — the caller includes a USDC payment header on Base (eip155:8453)
    or Solana (solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp) and the server verifies
    payment before processing. Free routes require no payment header. The same
    402 also carries an MPP (Machine Payments Protocol) challenge in
    WWW-Authenticate; an MPP client pays with USDC on Base by replaying with
    `Authorization: Payment ...`, and routes, prices, and responses are
    identical.


    ## Getting started


    To set up a wallet for making x402 payments, choose a provider:


    - **Locus** (default): https://paywithlocus.com/SKILL.md

    - **Sponge**: https://wallet.paysponge.com/skill.md — automatic x402 service
    discovery

    - **Ampersend**: https://www.ampersend.ai/getting-started.md — self-custody
    on Base or Solana with dual-approval spending limits. Laso Finance is a
    default skill, so no manual endpoint registration is needed.


    ## How x402 works


    1. Call a paywalled endpoint without a payment header → receive a `402
    Payment Required` response containing payment details (price, recipient
    address, network).

    2. Construct an x402 payment header using the details from the 402 response.

    3. Replay the request with the payment header → the server verifies payment
    and processes the request.


    ## Authentication flow


    `GET /auth` is free: callers prove wallet ownership by sending a
    `SIGN-IN-WITH-X` header (CAIP-122 wallet signature). Paywalled routes
    (`/get-card`, `/order-gift-card`, `/get-push-to-card`, `/order-intl-card`)
    also return fresh auth credentials in their responses, so a payment is never
    required just to obtain a token.


    Most routes return auth credentials (`id_token`, `refresh_token`,
    `expires_in`). Use the `id_token` as a Bearer token to call authenticated
    Laso Finance endpoints like `/get-card-data`. When the `id_token` expires,
    use `POST /auth` with `grant_type: refresh_token` to get a new one.


    ## Important notes


    The `/get-card` USA prepaid card endpoint is U.S. only — issued in USD,
    usable at U.S.-based merchants only, and physical goods must ship to a U.S.
    address. For non-U.S. merchants or non-USD currencies, use `GET
    /order-intl-card` instead (international prepaid card, admin-fulfilled
    within 24 hours). All cards are intended for the caller's own use.


    ## Rate limits


    Every response carries the request budget so you can pace yourself without
    probing for a limit:


    | Header | Meaning |

    | --- | --- |

    | `RateLimit-Limit` | Requests permitted per window |

    | `RateLimit-Remaining` | Requests still available |

    | `RateLimit-Reset` | Seconds until the window rolls over |

    | `RateLimit-Policy` | The policy these numbers describe, as
    `limit;w=seconds` |


    The same values are repeated as `X-RateLimit-*` for clients that only parse
    that spelling.


    Most routes advertise the service-wide ceiling. `POST /signup` enforces its
    own per-IP budget on top of it and overwrites these headers with its own
    numbers. `POST /refresh-card-data` is limited per card rather than per
    caller, so its headers keep the service-wide values and the per-card budget
    is reported only on rejection.


    Every rejection is a `429` carrying `Retry-After` in seconds and a matching
    `retry_after_seconds` field in the body, computed from the limit that
    actually rejected the request. Wait that long and retry once. Do not retry
    in a tight loop.


    ## API versioning


    Every response carries the contract version in an `X-Laso-Api-Version`
    header, and `GET /version` returns the same value as `api_version` alongside
    the policy below in machine-readable form. It is distinct from
    `X-Laso-Docs-Version`, which tracks the prose and moves whenever the
    documentation changes.


    | Change | What happens to `api_version` |

    | --- | --- |

    | New route, new optional request field, new response field | Unchanged.
    Tolerate unknown response fields. |

    | Breaking change | Ships under a new `api_version`. |


    A route being retired is marked `deprecated` in this description and answers
    with a `Deprecation: true` response header, plus a `Link` header with
    `rel="successor-version"` pointing at its replacement. Once a removal date
    is set it is published in a `Sunset` header (RFC 9745 / RFC 8594). There is
    no blanket notice period: the `Sunset` date on each deprecated route is the
    commitment.


    For step-by-step instructions, read https://laso.finance/SKILL.md
servers:
  - url: https://laso.finance
    description: Production
security: []
paths:
  /start-card-issuer-link:
    post:
      tags:
        - cards
      summary: Send the account holder a code to link the card issuer
      description: >-
        Starts the one-time card issuer link: the card issuer sends a one-time
        code to the account holder. Then call `POST /verify-card-issuer-link`
        with the code they enter.


        **The account holder, never the agent, receives and enters the code.**
        Send your human to their own inbox (or phone) and have them type the
        code into your interface. Never try to read the account holder's email
        or messages yourself, and never ask for their email password or access.


        **Where the code goes.** An account with an email on file always gets
        the code at that email; `email` and `phone` are ignored for it. An
        account with no email (a wallet-only sign-in) must send exactly one of
        `email` or `phone` (E.164). The response names the destination masked,
        never in full.


        **Limits.** Codes last 10 minutes. At most 5 codes can be sent per
        account in any rolling hour (`429`, with `Retry-After`). Sending a new
        code replaces the previous one, which then stops working.


        **Prerequisite:** Laso identity verification (`403 kyc_required`
        otherwise, with a `kyc_url`). If the account is already linked, this
        answers `200` with `status: linked` and sends nothing, so calling it
        twice is safe.


        **This route is free.**
      operationId: startCardIssuerLink
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  description: >-
                    Only for an account with no email on file. The account
                    holder's email.
                phone:
                  type: string
                  description: >-
                    Only for an account with no email on file, instead of
                    `email`. E.164, e.g. `+15551234567`.
            examples:
              account_with_email:
                value: {}
              wallet_only_account:
                value:
                  email: jane@example.com
      responses:
        '200':
          description: The code was sent, or the account is already linked.
          content:
            application/json:
              schema:
                type: object
                required:
                  - status
                  - note
                properties:
                  status:
                    type: string
                    enum:
                      - code_sent
                      - linked
                  channel:
                    type: string
                    enum:
                      - email
                      - phone
                  sent_to:
                    type: string
                    description: The destination, masked.
                  expires_at:
                    type: integer
                    description: >-
                      When the code stops working, in milliseconds since the
                      epoch.
                  note:
                    type: string
              examples:
                code_sent:
                  value:
                    status: code_sent
                    channel: email
                    sent_to: j***@e***.com
                    expires_at: 1790026656711
                    note: >-
                      A one-time code was sent to the account holder's email.
                      Ask the account holder to enter it in your interface, then
                      send it to POST /verify-card-issuer-link. Never read the
                      account holder's email yourself.
                already_linked:
                  value:
                    status: linked
                    note: >-
                      The card issuer is linked. Next, the account holder shares
                      their identity verification with the card issuer at
                      https://laso.finance/agent/dashboard/verified/card, then
                      the card balance can be funded with GET /fund-card-balance
                      and cards created with POST /create-reloadable-card.
          headers:
            X-Laso-Api-Version:
              $ref: '#/components/headers/X-Laso-Api-Version'
        '400':
          description: >-
            `code: contact_required`: the account has no email on file and the
            body did not carry exactly one of `email` or `phone`. `code:
            invalid_contact`: `email` or `phone` was not a string.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Provide exactly one of email or phone.
                code: contact_required
                hint: >-
                  This account has no email on file. Send exactly one of email
                  or phone (E.164, e.g. +15551234567) for the account holder.
          headers:
            X-Laso-Api-Version:
              $ref: '#/components/headers/X-Laso-Api-Version'
        '403':
          description: >-
            The account has not completed Laso identity verification (`code:
            kyc_required`). Nothing was sent. Hand `kyc_url` to the account
            holder, then poll `GET /get-kyc-status` until `kyc_verified` is
            `true`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: >-
                  Identity verification is required before linking the card
                  issuer.
                code: kyc_required
                kyc_url: https://verify.example.com/session/abc123
                hint: >-
                  The account holder must complete identity verification before
                  the card issuer can be linked. Send them kyc_url, then poll
                  GET /get-kyc-status until kyc_verified is true.
          headers:
            X-Laso-Api-Version:
              $ref: '#/components/headers/X-Laso-Api-Version'
        '409':
          description: >-
            A request with this `Idempotency-Key` is still being processed
            (`code: idempotency_key_in_progress`). Wait a moment and retry with
            the same key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: A request with this Idempotency-Key is still being processed
                code: idempotency_key_in_progress
          headers:
            X-Laso-Api-Version:
              $ref: '#/components/headers/X-Laso-Api-Version'
        '422':
          description: >-
            The `Idempotency-Key` was already used for a different request body
            or route (`code: idempotency_key_reused`). Use a new key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Idempotency-Key reused with a different request
                code: idempotency_key_reused
          headers:
            X-Laso-Api-Version:
              $ref: '#/components/headers/X-Laso-Api-Version'
        '429':
          description: >-
            `code: rate_limited`: 5 codes were already sent in the past hour.
            Wait `retry_after_seconds` (also in `Retry-After`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Too many codes sent. Wait and try again.
                code: rate_limited
                retry_after_seconds: 1200
                hint: >-
                  At most 5 codes can be sent per account in any rolling hour.
                  Wait retry_after_seconds before sending another. If a code was
                  already sent, ask the account holder for it instead.
          headers:
            X-Laso-Api-Version:
              $ref: '#/components/headers/X-Laso-Api-Version'
      security:
        - BearerAuth: []
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        maxLength: 255
      description: >-
        Optional client-generated unique key (a UUID works) that makes this
        write safe to retry. A retry with the same key within 24 hours returns
        the recorded response of the original request, marked with an
        `Idempotency-Replayed: true` header, instead of performing the write
        again. Reusing a key with a different body or route is rejected with 422
        (`code: idempotency_key_reused`); a retry that arrives while the
        original is still running gets 409 (`code:
        idempotency_key_in_progress`), so wait a moment and retry with the same
        key. Keys are scoped to the authenticated account.
  headers:
    X-Laso-Api-Version:
      description: >-
        The version of the API contract that produced this response, the same
        value `GET /version` returns as `api_version`. It moves only on a
        breaking change, so an agent can pin against it and tolerate unknown
        response fields without rechecking the docs. Distinct from
        `X-Laso-Docs-Version`, which tracks the prose. Present on every response
        once the service has read its version beacon; absent, rather than a
        placeholder, during the cold-start window before that read completes.
      schema:
        type: string
        example: 1.0.0
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
  securitySchemes:
    BearerAuth:
      type: apiKey
      in: header
      name: Authorization
      description: >-
        Firebase ID token from `/auth` or any paid route, sent as a Bearer
        token: `Authorization: Bearer <id_token>` (the `Bearer ` prefix is
        required).

````

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