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

# Send Instant On-Chain Transfer

> Moves USDC between two customer SCWs on the configured EVM chain via an ERC-4337 UserOperation. Settlement is on-chain and final within seconds — no banks, no correspondents, no cut-off times.

**Addressing:** `sender_phone` and `recipient_phone` each accept EITHER an E.164 phone number (`+2250700000001`) OR a 0x-prefixed 40-character SCW wallet address — the format is auto-detected, so there is no separate field to learn. Use whichever identifier you already have for that party.

**Sender** must be a user affiliated with your business (provisioned under or adopted by your account).

**Recipient can be ANY phone number or existing wallet address** — system-wide, merchant-agnostic. If the recipient belongs to another business, their business is notified by webhook. If addressed by phone and that number has never been provisioned by anyone, Rach creates an unaffiliated wallet on the fly (`recipient_status: PROVISIONED_ON_THE_FLY`) and notifies the recipient by SMS with instructions to access their money via Rach's USSD service. Addressing by wallet address requires that wallet to already exist — it cannot be auto-provisioned, since there's no phone number to derive a fresh identity from.

For fiat-denominated transfers supply a valid quote_id from POST /v1/fx/quote. For direct crypto amounts use quote_id format `DIRECT_CRYPTO:USDC:{amount}` (e.g. `DIRECT_CRYPTO:USDC:10.50`). AML daily/monthly limits apply per sender.



## OpenAPI

````yaml /api-reference/caas-openapi.json post /v1/transfers/send
openapi: 3.0.0
info:
  contact:
    name: Rach Finance Support
    url: https://rach.finance
  description: >-
    Rach CaaS is the stablecoin infrastructure layer for African fintechs and
    financial institutions. It enables any business to offer their customers
    non-custodial USDC and USDT wallets, instant on-chain transfers, and fiat ↔
    stablecoin conversion in any supported currency (XOF, XAF, NGN, GHS, ...) —
    without building blockchain infrastructure.


    **EVM stablecoins:** USDC and USDT are supported on the configured ERC-4337
    rail. On an EVM value endpoint — `POST /v1/users/fund`, `POST
    /v1/transfers/send`, `POST /v1/users/withdraw` — set the token
    (`target_token` / `token`, or the ticker inside a
    `DIRECT_CRYPTO:{USDC|USDT}:{amount}` quote). `GET /v1/users/balance` returns
    both `balance_usdc` and `balance_usdt`.


    **System-wide phone addressing:** wallets are keyed by phone number and
    belong to the system, not to any one business. Money moves freely across
    businesses — and to phone numbers that have never been provisioned: Rach
    creates the wallet on the fly and notifies the recipient by SMS with USSD
    access instructions.


    **Networks:** EVM settlement profiles are chain-scoped. Polygon PoS (137),
    Base (8453), and BNB Smart Chain (56) may be enabled together only after
    each has its own approved RPC, contracts, sponsor funding, and scan start
    block. External ERC-20 receipts are stored before their `transfer.received`
    webhook and include the rail name. TRON and Solana are parallel native
    rails.


    ## Native TRON USDT

    TRON mainnet is a parallel USDT TRC-20 rail, not an ERC-4337 chain profile.
    Use `/v1/tron/*` to provision a durable `T...` customer address, read its
    USDT contract balance, and submit a sponsored USDT send when the rail is
    ready. A customer may receive USDT and read its balance while holding **zero
    TRX**; Rach creates the account and sponsors Energy/Bandwidth only for an
    enabled transfer-out.


    When `/health/ready` returns `tron: INBOUND_READY`, native TRON read-side
    RPC and reader-account checks have passed, but `POST
    /v1/tron/transfers/send` is deliberately unavailable (503). The API
    readiness check does not certify the separately deployed custody or
    deposit-scanner workers; use a confirmed deposit record or
    `transfer.received` webhook as proof of an observed inbound USDT deposit.
    Only call a sponsored send after readiness reports `tron: READY`. This
    protects customer value while Rach funds or maintains sponsor capacity.


    ## Embedded Escrow

    `/v1/escrows` describes a partner API for dedicated Rach-custodied TRON USDT
    wallets. It is separate from the Rach P2P trading interface and is not a
    TRON smart-contract escrow. ChainZap or another partner can build its own
    interface around create, read, list, events, release, refund, dispute and
    cancel operations. Only TRON USDT has an adapter; EVM and Solana transfers
    do not imply escrow support on those rails. Both customer wallets must be
    provisioned and linked to the tenant first.


    New escrow admission requires enabled TRON deposits and sponsored payouts
    with a successful sponsor-readiness check. Release/refund settle to the
    linked customer's Rach wallet, not an arbitrary external wallet. Request
    amounts are whole-token strings; response amounts are base-unit strings. The
    per-deal inspection window starts at confirmed funding and defaults to no
    automatic release. The partner API key authorizes ordinary release/refund;
    on_behalf_of is a recorded assertion. Disputes cannot overtake claimed
    payouts. Rach staff can authorize separate recoveries of partial, excess and
    late deposits. Signed escrow webhooks have stable event IDs, durable retries
    and tenant-scoped replay. Custody, compliance, countries, dispute operating
    terms and prices require the partner agreement.


    ## Authentication

    **B2B API endpoints** (`/v1/users/*`, `/v1/transfers/*`, `/v1/fx/*`) use API
    Key authentication. Pass your key in the `X-API-Key` header. Keys are
    prefixed `rach_sk_live_` (mainnet) or `rach_sk_test_` (sandbox — no on-chain
    execution).


    ## Sandbox Mode

    Test-mode API keys (`rach_sk_test_*`) and account test mode prevent live B2B
    mutations. Supported mutations return simulated responses without blockchain
    execution; unsupported mutations return SANDBOX_UNSUPPORTED. Read requests
    pass through to their handlers and do not read a separate persistent
    sandbox. Escrow simulations create no stored deal, do not reproduce the
    asynchronous lifecycle or emit settlement webhooks, and must never be
    funded. A simulated escrow ID cannot subsequently be fetched as a stored
    escrow. Sandbox simulations do not establish full live-request validation or
    production readiness.


    ## Typical Integration Flow

    1. `POST /v1/users/provision` — create a non-custodial SCW for each end-user
    (offline, no gas)

    2. Collect fiat from customer → deposit to Rach bank account → your CaaS
    dashboard

    3. Rach confirms XOF receipt → your CaaS dashboard (credits your USDC
    balance)

    4. `POST /v1/users/fund` — transfer USDC from your treasury balance to the
    customer SCW on-chain

    5. `GET /v1/users/balance` — read live on-chain USDC balance through the
    active chain RPC

    6. `POST /v1/transfers/send` — instant peer-to-peer USDC transfer between
    customer SCWs (ERC-4337 UserOp)

    7. `POST /v1/users/withdraw` — off-ramp: sweep USDC from SCW back to Rach
    treasury; Rach pays out XOF via mobile money


    ## System-Wide Wallets & Multi-Membership

    A wallet is keyed by phone number and belongs to the **system**, not to any
    one business. Several businesses can operate the same wallet — but only with
    the customer's consent.


    **Provisioning outcomes** (`POST /v1/users/provision`):

    - `PROVISIONED` — new wallet created; you are its first member.

    - `ADOPTED` — the phone had an unaffiliated wallet (created on the fly by an
    inbound transfer); you are now its primary member.

    - `ALREADY_EXISTS` — the wallet exists and you already operate it.

    - `LINK_REQUIRED` — the wallet belongs to another business. You get the same
    address back but **no operating rights**. Start the consent flow to gain
    them.


    **Consent-based linking** (to operate a wallet another business created):

    1. `POST /v1/users/link/request` — a one-time code is SMSed to the customer.

    2. `POST /v1/users/link/confirm` — submit the code the customer gives you;
    you become a member. Existing members receive a `user.linked` webhook.


    **Money-in is open, money-out is gated.** Any business can fund a wallet or
    send money to it (paid from the sender's own ledger). Only a **member** may
    originate transfers, withdraw, or change the phone number for a wallet.


    **Consent-gated phone change** (`POST /v1/users/phone-change/request` →
    `/confirm`): a one-time code is SMSed to the NEW number (proving SIM
    control). The SCW address, balance and history are preserved; every member
    receives a `user.phone_changed` webhook.


    ## Webhook Events

    Signed with HMAC-SHA256 over the body in the `X-Rach-Signature: sha256=...`
    header (key = your webhook secret).

    - `transfer.received` — a wallet you operate received an inbound transfer.

    - `user.linked` — a new business was granted operating rights on a wallet
    you operate.

    - `user.phone_changed` — the phone number of a wallet you operate was
    re-keyed (address unchanged).


    ## Multi-Currency

    Rach moves any fiat; USDC is the settlement rail. Rates are per-currency
    (XOF, XAF, NGN, GHS, ...). See your CaaS dashboard. Topup quotes and
    withdrawals carry a `currency_code` / `payout_currency`.


    ## Solana (USDC & USDT)

    Solana mainnet is a parallel SPL rail, not an ERC-4337 chain profile. Use
    `/v1/solana/*` to derive a durable customer address, read USDC and USDT
    balances, and submit a sponsored send.


    Sponsorship is native here: every Solana transaction names a fee payer, and
    Rach is named as that payer. The customer signs only as token authority and
    needs **zero SOL** — there is no paymaster contract and no resource lease.
    Before a provision response succeeds, Rach finalizes the customer's USDC and
    USDT Associated Token Accounts, so exchanges can deposit to an otherwise
    empty owner address. That two-account rent is non-reclaimable and tracked in
    the Solana operator overview. If an outbound recipient has never held the
    mint, Rach also pays that separate one-off rent, reported as
    `created_recipient_ata`.


    When `/health/ready` returns `solana: INBOUND_READY`, zero-SOL provisioning
    and balance reads have passed and receipt monitoring is configured
    (`SOLANA_SCAN_ENABLED=true`), but `POST /v1/solana/transfers/send` is
    deliberately unavailable (503). That state does not certify the separately
    deployed scanner worker or webhook delivery; use a confirmed deposit record
    or `transfer.received` webhook as proof of an observed inbound credit. With
    monitoring disabled Solana reports `DEGRADED`, even where balance reads
    work. Only call a sponsored send after readiness reports `solana: READY`.
    This release supports SPL USDC/USDT only; native SOL payments and Solana
    swaps are separate products.


    ## EVM Fees & Merchant Pricing

    An ERC-4337 peer-to-peer transfer costs **0.30% of the amount, capped at
    1.20**. That is Rach's margin, deducted from the amount sent: the sender is
    debited exactly what they asked to send, and the recipient receives the
    remainder.


    **You can charge your own fee alongside Rach's and keep it.** Price it the
    same way Rach does, at your CaaS dashboard, along with an EVM address you
    control: either `mode: "PERCENTAGE"` — a rate in `fee_bps` bounded by
    `fee_cap_micros` (0 for uncapped) — or `mode: "FLAT"`, one fixed
    `fee_flat_micros` per transfer. The two are alternatives and never combine.


    Your fee is deducted from the amount sent and moves directly from the sender
    to your address in the same atomic on-chain batch as the transfer — Rach
    never takes custody of it, so there is no payout to request and no balance
    to reconcile. On a transfer too small to carry both fees your fee is waived
    rather than the recipient receiving nothing. Read what you have earned at
    your CaaS dashboard.


    Your percentage is capped at 1000 basis points (10%). That ceiling is a typo
    guard, not a pricing policy: a slipped digit in basis points is charged to
    your own customers at full value on every transfer until someone notices.


    Native TRON USDT sponsorship covers the customer's network costs
    (activation, Energy, and Bandwidth) when sends are enabled. It does **not**
    reuse an EVM merchant-fee address or deduct an undisclosed TRON business
    fee; native fee collection remains disabled until Rach configures an
    explicit TRON collection policy.


    ## Status Lifecycles

    Every money-moving call returns `202 Accepted` and settles asynchronously.
    Only two states are terminal — `SETTLED`/`COMPLETED`/`REFUNDED` and
    `FAILED`. Treat everything else as still in progress.


    **Deposit / funding:** `QUEUED` → `SUBMITTING` → `SUBMITTED` → `SETTLED`

    **Transfer:** `QUEUED` → `SUBMITTING` → `SUBMITTED` → `SETTLED`

    **Swap:** `QUEUED` → `SUBMITTING` → `SUBMITTED` → `SETTLED`

    **Withdrawal:** `PENDING` → `SUBMITTING` → `SUBMITTED` → `CRYPTO_RECEIVED` →
    `PAYOUT_SUBMITTING` → `COMPLETED`


    Any of them can end in `FAILED`, which carries an `error_reason`. A failed
    funding or transfer returns the value to your ledger.


    **Never retry on `SUBMISSION_UNKNOWN` or `PAYOUT_UNKNOWN`.** They mean Rach
    could not confirm whether an operation reached the network or the payout
    provider, so it may already have happened. Rach reconciles them
    automatically. Retrying with a *new* idempotency key risks paying twice;
    retrying with the *same* key is always safe.


    On a withdrawal, `CRYPTO_RECEIVED` means the crypto leg is done but the cash
    has not landed. `COMPLETED` requires recorded evidence that the payout
    reached the recipient. `REFUNDED` means the payout could not be delivered
    and the stablecoin went back to the customer wallet — distinct from
    `FAILED`, where nothing ever left it.
  title: Rach CaaS — Crypto-as-a-Service API
  version: 1.0.1
servers:
  - url: https://api.rach.finance/caas
security: []
paths:
  /v1/transfers/send:
    post:
      tags:
        - B2B - Transfers
      summary: Send Instant On-Chain Transfer
      description: >-
        Moves USDC between two customer SCWs on the configured EVM chain via an
        ERC-4337 UserOperation. Settlement is on-chain and final within seconds
        — no banks, no correspondents, no cut-off times.


        **Addressing:** `sender_phone` and `recipient_phone` each accept EITHER
        an E.164 phone number (`+2250700000001`) OR a 0x-prefixed 40-character
        SCW wallet address — the format is auto-detected, so there is no
        separate field to learn. Use whichever identifier you already have for
        that party.


        **Sender** must be a user affiliated with your business (provisioned
        under or adopted by your account).


        **Recipient can be ANY phone number or existing wallet address** —
        system-wide, merchant-agnostic. If the recipient belongs to another
        business, their business is notified by webhook. If addressed by phone
        and that number has never been provisioned by anyone, Rach creates an
        unaffiliated wallet on the fly (`recipient_status:
        PROVISIONED_ON_THE_FLY`) and notifies the recipient by SMS with
        instructions to access their money via Rach's USSD service. Addressing
        by wallet address requires that wallet to already exist — it cannot be
        auto-provisioned, since there's no phone number to derive a fresh
        identity from.


        For fiat-denominated transfers supply a valid quote_id from POST
        /v1/fx/quote. For direct crypto amounts use quote_id format
        `DIRECT_CRYPTO:USDC:{amount}` (e.g. `DIRECT_CRYPTO:USDC:10.50`). AML
        daily/monthly limits apply per sender.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/cmd_api.TransferRequest'
        description: Transfer payload
        required: true
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/cmd_api.TransferResponse'
          description: Accepted
        '400':
          content:
            application/json:
              schema:
                additionalProperties: true
                type: object
          description: Invalid payload or expired FX quote
        '401':
          content:
            application/json:
              schema:
                additionalProperties: true
                type: object
          description: Invalid or missing API key
        '404':
          content:
            application/json:
              schema:
                additionalProperties: true
                type: object
          description: Sender or recipient wallet not provisioned
        '409':
          content:
            application/json:
              schema:
                additionalProperties: true
                type: object
          description: Duplicate idempotency key
        '410':
          content:
            application/json:
              schema:
                additionalProperties: true
                type: object
          description: FX quote expired
        '429':
          content:
            application/json:
              schema:
                additionalProperties: true
                type: object
          description: 'AML block: daily or monthly limit exceeded'
      security:
        - ApiKeyAuth: []
components:
  schemas:
    cmd_api.TransferRequest:
      properties:
        chain:
          description: >-
            Chain selects the EVM settlement network: "polygon", "base" or
            "bsc".

            Omit it for the deployment's primary chain, which is what every
            existing

            integration gets. A chain that is not configured here is rejected
            rather

            than settled on the primary chain, because quietly moving a payment
            to a

            different network than the one asked for is a real loss of value.


            TRON and Solana are separate rails with their own endpoints under

            /v1/tron and /v1/solana, not values for this field.
          example: polygon
          type: string
        idempotency_key:
          example: txn_ref_abc123
          type: string
        local_fiat_amount:
          example: '5500.00'
          type: string
        quote_id:
          example: DIRECT_CRYPTO:USDC:0.50
          type: string
        recipient_phone:
          description: >-
            RecipientPhone identifies the recipient — same dual format.
            Addressing by

            phone allows on-the-fly provisioning for a never-seen number;
            addressing

            by wallet address requires that wallet to already exist.
          example: '+2250700000002'
          type: string
        sender_phone:
          description: >-
            SenderPhone identifies the sender — accepts EITHER an E.164 phone
            number

            (+2250700000001) OR a 0x-prefixed wallet address. No separate field

            needed: the format is auto-detected.
          example: '+2250700000001'
          type: string
        target_token:
          enum:
            - USDT
            - USDC
          example: USDC
          type: string
      required:
        - idempotency_key
        - local_fiat_amount
        - quote_id
        - recipient_phone
        - sender_phone
        - target_token
      type: object
    cmd_api.TransferResponse:
      properties:
        created_at:
          type: string
        message:
          type: string
        recipient_status:
          description: >-
            RecipientStatus is "EXISTING" when the recipient already had a
            wallet, or

            "PROVISIONED_ON_THE_FLY" when Rach auto-created an unaffiliated
            wallet for

            their phone number (recipient gets an SMS with USSD access
            instructions).
          example: EXISTING
          type: string
        status:
          type: string
        transfer_id:
          type: string
      type: object
  securitySchemes:
    ApiKeyAuth:
      description: >-
        Your Rach B2B API key. Use `rach_sk_live_*` for production (on-chain) or
        `rach_sk_test_*` for sandbox (no-chain simulation).
      in: header
      name: X-API-Key
      type: apiKey

````

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