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

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 |
| `SETTLED` / `COMPLETED` | **Yes**   | Final and confirmed                                       |
| `FAILED`                | **Yes**   | Final, did not happen, value returned                     |

### SUBMISSION\_UNKNOWN

This is the state most integrations get wrong.

It means a submission was sent but Rach could not confirm whether the network received it.
The operation may be live on-chain, or may not exist at all — and from outside, those look
identical.

<Warning>
  **Do not issue a new request in response to `SUBMISSION_UNKNOWN`.** Rach resolves it
  automatically by reconciling against the chain, and it will move to `SETTLED` or `FAILED`
  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.
</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                    |
| `COMPLETED`       | Done                  | Done — payout evidence recorded |

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

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