Pending Claims
Paying a phone number that resolves to no SafeBank recipient does not fail —
it opens a pending claim. The off-ramp payment parks in
PENDING_CLAIM, the amount is recorded against the claim for whoever proves
control of that number and completes onboarding, and if nobody does before
expiry the sender is refunded. (On-chain escrow custody of claim funds ships
with the claims increment; today the claim is a durable platform record.)
curl -X POST https://api.sandbox.safebank.ai/v1/offramp/payments \
-H "X-SafeBank-Api-Key: $SAFEBANK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone": "+15550000001", "amountUsd": "15.00" }'
{
"paymentId": "cpm_2b3c4d",
"status": "PENDING_CLAIM",
"claimId": "clm_5e6f7g",
"recipientPhoneMasked": "+1 ••• 0001",
"sourceAmountBaseUnits": "15000000",
"holdReasons": [],
"createdAt": "2026-08-21T17:10:41.000Z"
}
The opaque on-chain id
Each claim carries a 32-byte identifier (0x + 64 hex) generated from a
CSPRNG. It is the only value that joins the claim to the on-chain escrow,
and it is opaque by construction:
- It is never derived from the phone number — a bare phone hash would be brute-forceable over the ~10^10 possible numbers.
- The claim's
metadataHashis a sha256 over server-generated opaque ids only (the claim id and the payment id), so it is unguessable and phone-free by construction. - Nothing E.164-shaped ever appears in calldata, logs, or chain events; the end-to-end suite asserts this.
The SMS claim flow
The claim flow is designed as follows; the pieces marked (claims increment) ship with the claims track, on top of the claim records and notification hooks that exist today.
- Claim opened (shipped): the payment mints the claim row and enqueues a durable notification event. The phone number rides the internal alias record only — never the event payload.
- SMS with a one-time link (claims increment): the recipient gets a text carrying the amount and a claim link — no identity data. The token in the link is single-use, expiring, generated from a CSPRNG, and stored only as its sha256 — a leaked log or browser history cannot replay it.
- Open the link (claims increment):
GET /v1/claims/{claimToken}is public but returns no PII, is throttled, and is served withReferrer-Policy: no-referrerandCache-Control: no-store. - Prove control of the phone (claims increment): an OTP challenge is sent to the phone number from the claim record — never a client-supplied number. OTP sends are budgeted per phone and per IP.
- Onboard and choose a rail: the recipient authenticates with SafeBank, completes identity verification (KYC/KYB), and adds a payout destination.
- Release: the payment leaves
PENDING_CLAIMand re-enters the normal policy check — the full policy composer runs before any money moves. On-chain, the release commits the hash of that immutable policy decision.
The link alone releases nothing. Token, OTP, SafeBank authentication, identity verification, and destination consent are all required — a stolen SMS link is not a stolen payment.
Expiry and refunds
- Claims expire after 14 days by default (platform-configurable).
- An unfunded expired claim simply cancels the payment.
- A funded expired claim moves the payment
REFUND_PENDING → REFUNDED— money returns to the sender, never to a third party. - On-chain (claims increment): the escrow contract's expired-claim refund is permissionless and works even while the contract is paused, refunding only to the claim's recorded refund address. No SafeBank key is needed to recover expired funds — a lost operational key cannot strand them. A claim locked at the moment of expiry (a payout may be in flight) becomes refundable only after a grace period, so a refund can never race a legitimate release: funds win over expiry.
Claim lifecycle:
OPEN ──▶ LOCKED ──▶ RELEASED (claimed and paid out)
│ │
│ └──▶ REFUNDED (grace-period refund of a stuck lock)
└──▶ EXPIRED ──▶ REFUNDED (nobody claimed in time)
└──▶ CANCELED (unfunded)
RELEASED, REFUNDED, and CANCELED are absorbing — a claim can never be
both released and refunded. Status changes are driven by indexed chain
events, not by transaction submission.
Security properties
| Property | Mechanism |
|---|---|
| No phone data on-chain | Opaque CSPRNG claim id; salted metadata hash over opaque ids only |
| Claim link theft is not payment theft | One-time sha256-stored token + OTP to the recorded phone + full SafeBank auth + identity verification + destination consent, all before release |
| Replay-proof token | Atomic single-use consume; token stored only as a hash |
| SIM-swap resistance | OTP proves current control only — release still requires identity verification, binding fraud to real documents; rebound numbers force re-verification |
| No blind release | Every release re-runs the policy composer and commits the decision hash on-chain |
| No stranded funds | Permissionless expired-claim refund to the recorded refund address, working even while paused |
| Rate abuse | Per-phone OTP budgets, per-IP throttles, attempt counting before comparison |