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

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

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.