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

# Send Aro Mail

> Escrow cAROUSD into a printable letter, mail it through Lob or print it yourself, let the recipient claim it by QR or code, and track or void it along the way.

[Aro Mail](/concepts/aro-mail) delivers a private cAROUSD transfer on paper.
You create a letter (the value moves into escrow), get a one-page PDF with a QR
code and a 28-character claim code, and either print it or have AroPay mail it
through Lob. The recipient scans the QR, signs in, picks a wallet and claims;
the escrow releases into their wallet. This guide walks the whole loop with
`curl`; the **Aro Mail** pages in the dashboard do the same thing with buttons.

<Info>
  You need a **custodial** wallet holding enough cAROUSD (mint first if needed)
  plus a little ETH for the escrow transaction. Recipients need an AroPay account
  provisioned by Aro Media; there is no self-service sign-up, and a code stays
  claimable until it expires, so they can claim once their account exists.
</Info>

<Steps>
  <Step title="Create the letter">
    Post the amount, the sending wallet and the recipient's postal address.
    `country` is an ISO 3166-1 code; `postal` is optional outside the US as long
    as `region` is present; `landmark` prints as its own `Near …` line.

    ```bash theme={"system"}
    curl -s "$BASE/transfer/letter" \
      -H "Authorization: Bearer $KEY" \
      -H "content-type: application/json" \
      -d @- <<'JSON'
    {
      "walletId": "ckwlt0001abcd",
      "amount": "50",
      "recipient": {
        "name": "Jane Doe",
        "address": {
          "line1": "12 Adeola Odeku Street",
          "line2": "Victoria Island",
          "landmark": "Opposite Eko Hotel",
          "city": "Lagos",
          "region": "LA",
          "country": "NG"
        }
      },
      "memo": "Thanks for the referral",
      "amountVisibility": "sealed",
      "expiresInDays": 45
    }
    JSON
    ```

    ```json theme={"system"}
    {
      "ok": true,
      "data": {
        "letter": {
          "id": "cklet0001abcd",
          "status": "ESCROWED",
          "amount": "50.00",
          "token": "cAROUSD",
          "amountVisibility": "sealed",
          "isDomestic": false,
          "expiresAt": "2026-10-22T09:45:00.000Z",
          "claimCode": "7K3M-Q9TX-2ABF-H8RD-5CWN-VGP4-0J6Y",
          "claimUrl": "https://dev.aropay.aro.media/claim#7K3M-Q9TX-2ABF-H8RD-5CWN-VGP4-0J6Y",
          "pdfUrl": "https://dev.aropay.aro.media/api/v1/transfer/letter/cklet0001abcd/pdf",
          "escrow": { "transactionId": "cktxn0005abcd", "txHash": "0x4d1e…", "status": "CONFIRMED", "verified": true },
          "claim": null,
          "refund": null,
          "mail": null,
          "needsReview": false,
          "error": null
        }
      }
    }
    ```

    Save the id:

    ```bash theme={"system"}
    export LETTER="cklet0001abcd"
    ```

    The call blocks up to \~20 s for the escrow receipt. If it comes back as
    `DRAFT` with `escrow.status: "PENDING"`, poll `GET /transfer/letter/$LETTER`
    until it reads `ESCROWED`: that is when the escrow transfer has confirmed
    **and** its encrypted amount was verified against the letter.

    <Note>
      `claimCode` and `claimUrl` are shown to you here and on the detail read while
      the letter is live, never in the list. Anyone holding the code can claim the
      value once, so treat it like cash until it is in the envelope.
    </Note>

    `amountVisibility` defaults to `visible` for domestic letters and `sealed`
    for international ones; `expiresInDays` defaults to 90 and 45 respectively
    (1–180 allowed).
  </Step>

  <Step title="Print it yourself, or mail it through Lob">
    <CodeGroup>
      ```bash Download the PDF theme={"system"}
      curl -s "$BASE/transfer/letter/$LETTER/pdf" \
        -H "Authorization: Bearer $KEY" -o "aro-mail-$LETTER.pdf"
      ```

      ```bash Mail it with Lob theme={"system"}
      curl -s "$BASE/transfer/letter/$LETTER/mail" \
        -H "Authorization: Bearer $KEY" \
        -H "content-type: application/json" \
        -d '{"deliveryMethod":"tracked","provider":"lob"}'
      ```
    </CodeGroup>

    The PDF is a single US Letter page: sender and recipient blocks, the
    amount panel (sealed letters print `••••••• cAROUSD`), the QR, the claim
    code for manual entry, the expiry date and a one-time-claim warning. It is
    available from `ESCROWED` onwards; `DRAFT` and `FAILED` letters return
    `409 letter_not_printable`, and finished letters render with a `CLAIMED`,
    `VOID` or `EXPIRED` stamp so a reprint cannot pass as live.

    `POST /transfer/letter/{id}/mail` hands the same PDF to Lob and moves the
    letter to `MAILED`:

    | `deliveryMethod` | What Lob does                                                                                                                                            |
    | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `standard`       | USPS First Class, no tracking.                                                                                                                           |
    | `tracked`        | USPS First Class plus **Certified** (US destinations) or **Registered** (international); `mail.trackingNumber` and tracking events appear on the letter. |
    | `courier`        | Not offered; returns `400 delivery_method_unavailable`.                                                                                                  |

    The response carries `mail.providerId`, `mail.status`,
    `mail.expectedDeliveryDate` and `mail.testMode`. With a Lob **test** key the
    letter is validated and priced but never printed or mailed
    (`testMode: true`); use it to rehearse. Mailing requires the deployment's
    public claim origin to be `https:` (`400 claim_origin_not_public`), and
    the request is idempotent: retrying after a timeout adopts the letter Lob
    already accepted instead of mailing a second copy.
  </Step>

  <Step title="The recipient claims">
    The recipient scans the QR or opens the link. It lands on the public
    **claim page** on the AroPay host with the code in the URL fragment (never
    sent to the server), shows who the letter is from and, for visible letters,
    how much, then asks them to sign in and pick a wallet. If the camera cannot
    read the QR, they type the code from the letter.

    Programmatically, the same two calls back the page:

    <CodeGroup>
      ```bash Preview (public) theme={"system"}
      curl -s "$BASE/transfer/letter/claim/preview" \
        -H "content-type: application/json" \
        -d '{"claimCode":"7K3M-Q9TX-2ABF-H8RD-5CWN-VGP4-0J6Y"}'
      ```

      ```bash Claim (recipient's credentials) theme={"system"}
      curl -s "$BASE/transfer/letter/claim" \
        -H "Authorization: Bearer $RECIPIENT_KEY" \
        -H "content-type: application/json" \
        -d '{"claimCode":"7K3M-Q9TX-2ABF-H8RD-5CWN-VGP4-0J6Y","walletId":"ckwlt0004abcd"}'
      ```
    </CodeGroup>

    ```json theme={"system"}
    {
      "ok": true,
      "data": {
        "preview": {
          "state": "claimable",
          "amount": null,
          "token": "cAROUSD",
          "amountVisibility": "sealed",
          "senderName": "Acme Payroll, Inc.",
          "memo": null,
          "expiresAt": "2026-10-22T09:45:00.000Z",
          "claimedAt": null,
          "chainId": 11155111
        }
      }
    }
    ```

    The claim returns `{ letter, transaction }`: the letter as `CLAIMED` (with
    the sealed `amount` and `memo` now revealed) and the `LETTER_CLAIM`
    transaction that credited the wallet. Codes are forgiving on input
    (case, dashes, spaces, `O`/`0` and `I`/`L`/`1` mix-ups are corrected before
    the checksum is checked). The claim is idempotent for the same recipient;
    a second scan by anyone else gets `409 already_claimed`. Senders cannot
    claim their own letters (`400 cannot_claim_own_letter`).

    <Tip>
      Claiming into an **external** (watch-only) wallet works, but AroPay cannot
      show or move those funds afterwards. Custodial wallets keep the value inside
      the dashboard, where the recipient can transfer it privately or
      [redeem](/guides/redeem-carousd) it.
    </Tip>
  </Step>

  <Step title="Track, and void if you need to">
    List your letters or read one; the detail read also refreshes Lob's status
    for mailed letters and finishes any in-flight leg:

    ```bash theme={"system"}
    curl -s "$BASE/transfer/letter?status=MAILED&pageSize=20" -H "Authorization: Bearer $KEY"
    curl -s "$BASE/transfer/letter/$LETTER" -H "Authorization: Bearer $KEY" \
      | jq '.data.letter | {status, mail, claim, refund, needsReview}'
    ```

    Each leg (`escrow`, `claim`, `refund`) points at a normal
    [transaction](/concepts/transactions) row (`LETTER_ESCROW`, `LETTER_CLAIM`,
    `LETTER_REFUND`) with its hash and explorer link, so receipts and history
    work exactly as for any other transfer.

    To cancel an unclaimed letter and get the value back:

    ```bash theme={"system"}
    curl -s "$BASE/transfer/letter/$LETTER/void" \
      -H "Authorization: Bearer $KEY" \
      -H "content-type: application/json" \
      -d '{"acknowledgeMailed":true}'
    ```

    Void works from `ESCROWED` at any time. Once `MAILED` you must send
    `acknowledgeMailed: true`, confirming you understand the paper letter will
    stop working (`409 letter_not_claimable` for whoever scans it); AroPay also
    asks Lob to cancel the mailing on a best-effort basis. The refund is a
    `LETTER_REFUND` transfer from the escrow vault back to the sending wallet's
    address; if it fails on-chain the letter stays `VOIDED` and the sweep
    retries the refund.

    Letters nobody claims expire at `expiresAt` and refund the sender the same
    way. Expiry is detected on read and by the operator's scheduled sweep.
  </Step>
</Steps>

## Failure modes

| Response                                                         | Meaning                                                                                                                                                                                                                                                |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400 invalid_postal_address`                                     | The address fails the country's rules (US: two-letter state, ZIP, 64-character lines; elsewhere: `region` or `postal` required; `line2 + landmark` too long), or Lob rejected it at mailing time.                                                      |
| `400 country_not_supported`                                      | The destination is blocked or not allow-listed on this deployment. `GET /transfer/letter/rules` lists the effective allow and block lists.                                                                                                             |
| `400 invalid_amount`                                             | Malformed amount, or above the per-letter cap (10,000 cAROUSD by default; `maxAmount` in `GET /transfer/letter/rules`).                                                                                                                                |
| `400 insufficient_confidential_balance`                          | The wallet cannot fund the escrow, or the escrow transfer clamped to zero. The letter is `FAILED`; nothing moved.                                                                                                                                      |
| `400 letter_not_escrowed` / `letter_already_mailed`              | `/mail` or `/void` on a letter that is not in an allowed state.                                                                                                                                                                                        |
| `400 void_requires_acknowledgement`                              | Voiding a `MAILED` letter without `acknowledgeMailed: true`.                                                                                                                                                                                           |
| `400 delivery_method_unavailable`                                | `courier` is not offered.                                                                                                                                                                                                                              |
| `400 claim_origin_not_public`                                    | Mailing requested, but the letter's claim origin is not `https:` (local or misconfigured deployment).                                                                                                                                                  |
| `400 invalid_claim_code`                                         | The code is malformed or its checksum does not match.                                                                                                                                                                                                  |
| `400 cannot_claim_own_letter`                                    | The claimant is the sender, or the chosen wallet is the sending address. Void instead.                                                                                                                                                                 |
| `402 postage_unavailable`                                        | Lob is unavailable or rejected the letter for a non-address reason; the letter stays `ESCROWED`, retry or self-print.                                                                                                                                  |
| `404 letter_not_found`                                           | Unknown letter id (or claim code).                                                                                                                                                                                                                     |
| `409 already_claimed` / `letter_not_claimable`                   | Someone else already claimed, or the letter is `DRAFT`, `VOIDED` or `FAILED`.                                                                                                                                                                          |
| `409 letter_not_printable`                                       | PDF requested for a `DRAFT` or `FAILED` letter.                                                                                                                                                                                                        |
| `410 letter_expired`                                             | Claimed after `expiresAt`; the sender is refunded.                                                                                                                                                                                                     |
| `429 letter_velocity_limit`                                      | Too many letters in flight for your account or that country; wait for some to be claimed, voided or expired.                                                                                                                                           |
| `502 transfer_failed` / `claim_failed`                           | The escrow or release transfer reverted on-chain. A reverted release puts the letter back to claimable; retry the claim.                                                                                                                               |
| `502 escrow_unsettled`                                           | The escrow transfer was broadcast (hash in the message) but could not be recorded fully. Value has left your wallet; the letter exists as `DRAFT` and finishes on its next read. Do not create it again: open it from `GET /transfer/letter` and poll. |
| `502 claim_unsettled`                                            | The release transfer was broadcast but could not be recorded fully. The letter stays `CLAIMING` for you; call claim again (or refresh) and reconciliation returns the completed claim.                                                                 |
| `503 escrow_vault_underfunded`                                   | The platform vault cannot cover the payout right now; the letter stays claimable. Platform-side.                                                                                                                                                       |
| `503 mail_provider_not_configured` / `public_url_not_configured` | Lob credentials, the return address or `AROPAY_PUBLIC_URL` are missing on the deployment. Platform-side.                                                                                                                                               |

Full definitions in [Errors](/api-reference/errors); the concept page covers
the [status machine](/concepts/aro-mail#status-machine) and the
[money-safety rules](/concepts/aro-mail#status-machine) behind these codes.
