> ## 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.

# Idempotency & settlement

> How to retry safely, what each status means, and how to build a CaaS integration that never double-pays.

CaaS moves real money asynchronously. Every money-moving call returns `202 Accepted` before
anything reaches the chain, and the outcome arrives seconds later by polling or webhook.

That gap is where integrations go wrong. This page covers the three things that make an
integration correct: **idempotency**, **knowing which states are terminal**, and **knowing
what is safe to retry**.

## Idempotency keys

Every money-moving endpoint takes a caller-supplied key. It is the single most important
field in the request.

| Endpoint | Field |
| - | - |
| `POST /v1/users/fund` | `deposit_id` |
| `POST /v1/transfers/send` | `idempotency_key` |
| `POST /v1/swaps` | `idempotency_key` |
| `POST /v1/users/withdraw` | `idempotency_key` |
| `POST /v1/solana/transfers/send` | `idempotency_key` |

The key is **yours to choose** and should be derived from something stable in your own
system — an order id, a payout row id, a ticket reference. Never generate it fresh on retry;
that defeats the entire mechanism.

<Warning>
  A retry with a **new** key is a **new payment**. If a request times out and you retry with a
  different key, you will send the money twice. Reuse the original key and CaaS will recognise
  the request.
</Warning>

### Keys are scoped to your business

Your idempotency keys live in your own namespace. `invoice-1001` used by another Rach
partner does not collide with yours, and never blocks you from using that value.

<Note>
  This is a change from earlier behaviour, where keys were unique platform-wide and one
  partner could inadvertently reserve a common string like `order-1` for everyone. If you
  previously worked around that by prefixing keys, the workaround is harmless but no longer
  necessary.
</Note>

### What a duplicate returns

CaaS distinguishes "still running" from "already done":

<CodeGroup>
  ```json 409 — still in flight theme={null}
  { "error": "Transfer already in progress or completed" }
  ```

  ```json 202 — already recorded theme={null}
  {
    "transfer_id": "rach_txn_7c1e",
    "status": "SETTLED",
    "message": "Transfer already exists for this idempotency key."
  }
  ```
</CodeGroup>

A `202` duplicate returns the **original** resource and its current status — not a new one.
That makes the safe retry pattern simple: resend the identical request, and use whatever
comes back.

A `409` means an identical request is being processed right now. Wait briefly and poll the
resource rather than resending immediately.

<Tip>
  Both responses are successful outcomes for your integration. Treat `409` as "it's handled,
  go look it up", not as an error to surface to a user.
</Tip>

## Settlement states

Only two states are terminal. Everything else means "keep waiting".

| Status | Terminal? | Meaning |
| - | - | - |
| `QUEUED` / `PENDING` | No | Durably recorded, not yet sent to the chain |
| `SUBMITTING` | No | Being submitted right now |
| `SUBMITTED` | No | On-chain, awaiting confirmation |
| `SUBMISSION_UNKNOWN` | No | Rach could not confirm the submission reached the network |
| `CRYPTO_RECEIVED` | No | Withdrawals — crypto swept, cash not sent yet |
| `PAYOUT_SUBMITTING` | No | Withdrawals — cash leg dispatched to a payout provider |
| `PAYOUT_UNKNOWN` | No | Withdrawals — Rach could not confirm the payout reached the provider |
| `SETTLED` / `COMPLETED` | **Yes** | Final and confirmed |
| `REFUNDED` | **Yes** | Withdrawals — payout undeliverable, stablecoin returned to the wallet |
| `FAILED` | **Yes** | Final, did not happen, value returned |

### SUBMISSION\_UNKNOWN and PAYOUT\_UNKNOWN

These are the states most integrations get wrong.

`SUBMISSION_UNKNOWN` means a submission was sent but Rach could not confirm the network
received it. `PAYOUT_UNKNOWN` is the same situation on a withdrawal's cash leg: the payout
was dispatched, but the provider's response never arrived.

In both cases the operation may have happened, or may not exist at all — and from outside,
those look identical.

<Warning>
  **Do not issue a new request in response to either.** Rach resolves them automatically — by
  reconciling against the chain, or by asking the payout provider what happened — and they move
  to a terminal state without any action from you.

  Sending a fresh request with a new idempotency key while the original may be in flight is how
  customers get paid twice. On the cash leg that is worse than on-chain: a duplicate payout to
  a recipient's bank or wallet cannot be reversed.
</Warning>

Resending the **same** idempotency key is always safe, and is the correct move if you are
unsure whether your original request was even accepted.

### Failure returns your money

`FAILED` is clean. For funding and transfers, the value is returned to your ledger as part
of the same failure — you are never left short, and there is no reconciliation step on your
side. The `error_reason` field explains what happened.

## Polling vs webhooks

Prefer [webhooks](/guides/webhooks): Rach pushes the terminal state and you do no work while
waiting.

Poll when you need a synchronous answer for a user staring at a screen. If you poll:

* Back off. These settle in seconds, not milliseconds — every 2–3 seconds is plenty.
* Stop at a terminal state. Polling a `SETTLED` resource forever costs you rate limit for
  nothing.
* Do not treat a slow settlement as a failure. There is no timeout after which an operation
  becomes safe to re-send; only `FAILED` means it did not happen.

<Note>
  Webhooks and polling agree, but webhooks can arrive before your own `202` handler has
  finished writing to your database. Make your webhook handler tolerant of an unknown id
  rather than treating it as an error — retry it, or reconcile on the next poll.
</Note>

## Withdrawals settle in two stages

An off-ramp has a crypto leg and a cash leg, and they complete at different times.

| Status | Crypto | Cash |
| - | - | - |
| `CRYPTO_RECEIVED` | Done — swept on-chain | Not yet sent |
| `PAYOUT_SUBMITTING` | Done | Dispatched to a payout provider |
| `PAYOUT_UNKNOWN` | Done | Dispatched, delivery unconfirmed |
| `COMPLETED` | Done | Done — payout evidence recorded |
| `REFUNDED` | Returned to the wallet | Never sent |

`COMPLETED` is deliberately strict: it requires durable, operator-attributed evidence that
the local-currency payout actually reached the recipient. A customer asking "where is my
money?" is answered by `CRYPTO_RECEIVED` — the crypto has moved, the cash is on its way.

Do not tell a customer they have been paid until the withdrawal reaches `COMPLETED`.

### When a payout cannot be delivered

A closed account, a dead wallet, or a compliance block means the cash can never arrive. Rach
returns the stablecoin to the customer's wallet and the withdrawal ends as `REFUNDED`.

`REFUNDED` is deliberately distinct from `FAILED`: **failed** means nothing ever left the
wallet, **refunded** means it left and came back. Your customer holds crypto again, not cash,
so they may want to retry with different payout details.

## Sandbox does not exercise any of this

A `rach_sk_test_` key returns `SANDBOX_SIMULATED` immediately, without touching a chain.
It validates your request shape, authentication and idempotency handling — and nothing else.

Sandbox will never give you `SUBMITTING`, `SUBMISSION_UNKNOWN`, or `FAILED`. Write and test
that handling deliberately, because sandbox will not force you to discover it.

## A correct integration, end to end

<Steps>
  <Step title="Derive a stable key">
    Use your own order or payout id. Store it before you call CaaS, so a crash mid-request
    still leaves you able to retry with the same key.
  </Step>

  <Step title="Call and record the 202">
    Store the returned resource id against your order immediately.
  </Step>

  <Step title="On any network error, resend the identical request">
    Same key, same body. You will get either the original resource (`202`) or a `409`.
    Neither is a double-spend.
  </Step>

  <Step title="Wait for a terminal state">
    Take `SETTLED`/`COMPLETED` or `FAILED` from a webhook, or poll until you see one. Never
    infer an outcome from elapsed time.
  </Step>

  <Step title="Act only on the terminal state">
    Release goods, notify the customer, or return the failure. A non-terminal state is not a
    result.
  </Step>
</Steps>


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