Skip to main content
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. 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.
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.

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

What a duplicate returns

CaaS distinguishes “still running” from “already done”:
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.
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.

Settlement states

Only two states are terminal. Everything else means “keep waiting”.

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

Withdrawals settle in two stages

An off-ramp has a crypto leg and a cash leg, and they complete at different times. 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

1

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

Call and record the 202

Store the returned resource id against your order immediately.
3

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

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

Act only on the terminal state

Release goods, notify the customer, or return the failure. A non-terminal state is not a result.