Skip to content

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:

  1. A zap fails and leaves an open credit.
  2. Routes creates a recovery record (recovery_id) bound to account_id + credit_id.
  3. Integrator activates the recovery with one authenticated call.
  4. Routes sends a one-time password (OTP) to the end user's email.
  5. End user calls the public claim endpoint with the OTP plus destination details.
  6. 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 -> active via POST /recoveries/{recovery_id}/activate
  • active -> claimed via POST /public/recover-funds
  • Claim errors emit recovery.failed event and may set status failed for 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-Key required

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:

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 active and the request binds to the same account_id + credit_id, Routes returns 200 and sends a fresh OTP, even with a new Idempotency-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:

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_claimed responses include the original recovery_id and cancel_id in error.details when 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.created
  • recovery.claimed
  • recovery.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 state
  • recovery.* and cancel.* webhooks for asynchronous updates