Recoveries¶
Recoveries handle a zap edge case where the zap fails and leaves an open credit_id in an asset the integrator cannot surface in product UX.
Recovery requires an email address on the account. Attach one via PATCH /accounts/{account_id} post-registration.
The flow:
- A zap fails and leaves an open credit.
- Routes creates a recovery record (
recovery_id) bound toaccount_id + credit_id. - Integrator activates the recovery with one authenticated call.
- Routes sends a one-time password (OTP) to the end user's email.
- End user calls the public claim endpoint with the OTP plus destination details.
- Routes screens the destination and executes a cancel-style transfer for the full credit amount.
If the account does not have an email attached, recovery activation returns email_not_configured. In this case, the integrator can move the credit on the user's behalf using POST /cancels — no separate recovery flow needed.
Recoveries do not expire automatically. A recovery remains claimable while the underlying credit remains open and unclaimed.
Capability discovery¶
Recovery is an organization-level feature flag.
- If recovery is enabled, failed zaps that leave an open credit include
recovery_id+credit_id. - If recovery is not enabled, failed zaps do not include recovery metadata.
- Routes does not currently expose a dedicated capabilities endpoint for recovery enablement.
Recovery statuses¶
| Status | Meaning |
|---|---|
created |
Recovery record exists and awaits activation |
active |
Recovery activated and OTP sent to end user |
claimed |
Public claim accepted; cancel-style transfer created |
failed |
Claim attempt failed |
Status transition model:
created -> activeviaPOST /recoveries/{recovery_id}/activateactive -> claimedviaPOST /public/recover-funds- Claim errors emit
recovery.failedevent and may set statusfailedfor the attempt record while leaving the underlying credit open when applicable
There is currently no GET /recoveries/{recovery_id} read endpoint. Track lifecycle via response payloads and recovery.* webhooks.
POST /recoveries/{recovery_id}/activate¶
Use this to activate a created recovery record. Routes sends an OTP to the email on file for the account.
Auth:
- Bearer token required
Idempotency-Keyrequired
Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
account_id |
string | Yes | Account bound to this recovery |
credit_id |
string | Yes | Open credit bound to this recovery |
Example request:
curl -X POST https://routes.srcry.xyz/v1/recoveries/rcv_01J.../activate \
-H "Authorization: Bearer $ROUTES_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-key-rcv-123" \
-d '{
"account_id": "acct_01J...",
"credit_id": "cred_01J..."
}'
200 example:
{
"request_id": "req_01J...",
"recovery": {
"recovery_id": "rcv_01J...",
"account_id": "acct_01J...",
"credit_id": "cred_01J...",
"status": "active",
"otp_sent": true,
"activated_at_ms": 1730000000000
}
}
Response fields¶
| Field | Type | Description |
|---|---|---|
request_id |
string | Request identifier |
recovery.recovery_id |
string | Recovery identifier |
recovery.account_id |
string | Account identifier |
recovery.credit_id |
string | Credit identifier |
recovery.status |
string | active after successful activation |
recovery.otp_sent |
boolean | true when OTP was sent to the account's email |
recovery.activated_at_ms |
integer | Activation timestamp |
Idempotency semantics:
- same key + same payload -> same activation response
- same key + different payload ->
409 idempotency_key_reuse
Activation retry semantics:
- If recovery is already
activeand the request binds to the sameaccount_id+credit_id, Routes returns200and sends a fresh OTP, even with a newIdempotency-Key. Use this to re-send the OTP if the end user didn't receive it.
Activation error cases:
| Code | HTTP | Description |
|---|---|---|
recovery_not_found |
404 | Recovery ID is unknown or does not belong to your organization |
credit_already_consumed |
409 | Recovery credit is no longer open |
email_not_configured |
422 | Account does not have an email attached. Use POST /cancels to move the credit instead. |
invalid_parameter |
400 | Malformed binding fields |
idempotency_key_reuse |
409 | Same key reused with different payload |
POST /public/recover-funds¶
Use this to claim an active recovery credit and send it to an inline destination address.
This endpoint is the only destination inline-address exception. Trade/zap/cancel endpoints still require pre-registered destination_id.
Auth:
- No bearer token
- OTP code in request body
Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
account_id |
string | Yes | Account identifier bound to this recovery |
credit_id |
string | Yes | Open credit to recover |
otp_code |
string | Yes | One-time password sent to the end user's email during activation |
destination.address |
string | Yes | Destination address for recovered funds |
destination.memo |
string | No | Memo field when required by destination chain |
destination.tag |
string | No | Tag field when required by destination policy. For XRPL, tag is protocol-optional but may be required by the receiving platform. |
Example request:
curl -X POST https://routes.srcry.xyz/v1/public/recover-funds \
-H "Content-Type: application/json" \
-d '{
"account_id": "acct_01J...",
"credit_id": "cred_01J...",
"otp_code": "483291",
"destination": {
"address": "bc1q...",
"memo": null,
"tag": null
}
}'
202 example:
{
"request_id": "req_01J...",
"recovery": {
"recovery_id": "rcv_01J...",
"account_id": "acct_01J...",
"credit_id": "cred_01J...",
"status": "claimed",
"asset_key": "spl.solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"amount_atoms": "5000000000",
"cancel_id": "cxl_01J...",
"poll_after_ms": 10000
}
}
Response fields¶
| Field | Type | Description |
|---|---|---|
request_id |
string | Request identifier |
recovery.recovery_id |
string | Recovery identifier |
recovery.account_id |
string | Account identifier |
recovery.credit_id |
string | Credit identifier |
recovery.status |
string | claimed on successful claim |
recovery.asset_key |
string | Recovered asset |
recovery.amount_atoms |
string | Full recovered credit amount |
recovery.cancel_id |
string | Cancel-style transfer identifier |
recovery.poll_after_ms |
integer | Milliseconds until next status poll |
Replay behavior:
- Claiming an already-claimed recovery returns
409 recovery_already_claimed. - Replay attempts never create additional payouts.
409 recovery_already_claimedresponses include the originalrecovery_idandcancel_idinerror.detailswhen available.
Example duplicate-claim response:
{
"error": {
"type": "invalid_request",
"code": "recovery_already_claimed",
"message": "Recovery was already claimed.",
"request_id": "req_01J...",
"details": {
"recovery_id": "rcv_01J...",
"cancel_id": "cxl_01J..."
}
}
}
Error cases¶
| Code | HTTP | Description |
|---|---|---|
invalid_otp |
401 | OTP is incorrect |
otp_expired |
401 | OTP has expired — call POST /recoveries/{recovery_id}/activate again to re-send |
recovery_not_found |
404 | No active recovery exists for this account/credit pair |
recovery_already_claimed |
409 | Recovery already claimed |
credit_already_consumed |
409 | Credit is no longer open |
destination_screening_failed |
403 | Destination failed screening checks |
invalid_parameter |
400 | Malformed destination or body fields |
rate_limited |
429 | Request throttled by abuse controls |
Rate limits¶
Public recovery endpoints are protected by low abuse-control limits, including per-IP throttling and per-credit single-consume guards. Exact thresholds are not part of the public contract.
Integrator cancel fallback¶
When the account does not have an email attached, self-service recovery via OTP is not available. In this case, the integrator should use POST /cancels to move the open credit to a pre-registered destination on the user's behalf. This is an authenticated operation using the integrator's API key — no recovery activation or OTP is needed.
Recovery event hooks¶
Recovery lifecycle events are delivered via webhooks:
recovery.createdrecovery.claimedrecovery.failed
Use these for audit trails and partner-side reconciliation.
Status tracking model¶
Public claim callers receive cancel_id in successful claim responses. Cancel status is an authenticated API surface; integrators should expose transfer progress and finality to end users using their own authenticated status UX backed by:
GET /cancels/{cancel_id}for canonical transfer staterecovery.*andcancel.*webhooks for asynchronous updates