Skip to main content

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

  1. 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.
  2. 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.
  3. Open the link (claims increment): GET /v1/claims/{claimToken} is public but returns no PII, is throttled, and is served with Referrer-Policy: no-referrer and Cache-Control: no-store.
  4. 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.
  5. Onboard and choose a rail: the recipient authenticates with SafeBank, completes identity verification (KYC/KYB), and adds a payout destination.
  6. Release: the payment leaves PENDING_CLAIM and 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

PropertyMechanism
No phone data on-chainOpaque CSPRNG claim id; salted metadata hash over opaque ids only
Claim link theft is not payment theftOne-time sha256-stored token + OTP to the recorded phone + full SafeBank auth + identity verification + destination consent, all before release
Replay-proof tokenAtomic single-use consume; token stored only as a hash
SIM-swap resistanceOTP proves current control only — release still requires identity verification, binding fraud to real documents; rebound numbers force re-verification
No blind releaseEvery release re-runs the policy composer and commits the decision hash on-chain
No stranded fundsPermissionless expired-claim refund to the recorded refund address, working even while paused
Rate abusePer-phone OTP budgets, per-IP throttles, attempt counting before comparison