Prerequisites
- Node >= 20; pure ESM, strictly typed TypeScript.
- SDK v0.0.2 (
@0xramp/sdk). Install from repo (see Quickstart). - Issued by 0xramp:
partnerId, enabled corridor, API origin, pane origins. - Your app provides: ZEC wallet core (signs transparent-address sends), secure storage scoped to one unlocked wallet, a WebView with navigation control (desktop) or your platform’s auth-session opener (
openAuthSessionAsync, mobile browser-hosted mode), and a return-link scheme you register and route yourself. - Attribution: render “Powered by 0xramp . P2P.me” at the ramp entry point.
Terminology
Environment configuration
Two environments:"production" and "staging".
apiBaseUrl/paneOrigins must be https origins (no credentials, path, query, fragment). All API requests use redirect: "error" and no automatic retries.
Durable send journal
Any bridge with a signing callback requires asendStore. Construct one per unlocked wallet namespace:
createMemoryZecSendStore() is for tests only.
Happy path (SELL ZEC to fiat)
Step 1 — Create the session
Persist the creation intent before the POST, then create:{ sessionUrl, sessionRef, statusTicket, expiresAt }. Persist the full session securely.
Failures: ConfigError, ApiError (.status has HTTP code), PartnerQuotaExceeded (429), NetworkUnavailable (timeout — POST is unresolved), OriginLockViolation.
The returnUrl scheme must be letter-first per RFC 3986. createSession rejects digit-first schemes and values without a scheme prefix with ConfigError before any POST; https: URLs and schemes without // are accepted. See Scheme rules.
Lost create response: keep the persisted intent and reconcile with 0xramp before another create. partnerSessionId is correlation data, not a recovery credential.
Step 2 — Attach the bridge
Attach before loading the WebView, bound to this session:sessionRef explicitly whenever more than one session exists. Close the old bridge before switching sessions. psp/close closes the bridge before calling onClose.
Step 3 — Implement transport and load pane
The transport interface:webViewRef.current.postMessage(JSON.stringify(message)) for host-to-pane; onMessage with ramp.isAllowedPaneUrl(event.nativeEvent.url) validation for pane-to-host.
Electron: IPC in main process; verify event.sender, sender frame, and frame URL.
Load the pane:
Step 4 — Handle the send request
The pane sendspsp/zec-send-request with { requestId, address, amountZat, memo? }. Your handler returns:
Preserve all transaction IDs as an array. Never join them into one string. Identify the deposit from wallet history, or stay pending.
Step 5 — Resolve uncertain sends
Pending means reconcile, do not send again. Once wallet history proves the deposit:Step 6 — Read authoritative status
created -> opened -> user-active -> { settled | failed | expired | cancelled }. Refresh on advisory psp/result, app foreground, return link, and user request. expired is reversible. fiat.amountDisplay is a receipt string — never do math on it.
Step 7 — Restore on restart
Step 8 — Handle return deep-link
Validate scheme/host yourself, matchsessionRef against your saved session, then:
getStatus and your chain view.
Mobile: browser-hosted pane
On mobile, open the pane in the system browser (an OS auth-session) instead of a WebView. This is a supported hosting mode, not a fallback rung — passkey-first products need it. Only a real browser engine can create and assert the pane’s WebAuthn credential: Android WebView has no WebAuthn support, and iOS WKWebView would need awebcredentials association file for 0xramp.app that 0xramp does not serve. Web sessions do not cross WebView and browser contexts, so pick one surface per session.
Pane readiness (current state). The deployed pane does not yet implement this mode’s browser path: outside a native WebView, the deposit step shows a disabled action and the message “Open this session inside your partner wallet” (no QR, no copy fallback), and nothing in the pane navigates to your returnUrl — the value is validated at session create only, so the browser sheet never closes itself at flow end. QR/copy without a bridge and end-of-flow return navigation are planned pane-side work. The sections below describe the target design and the integration shape to build against.
There is no bridge in this mode. The flow is create, open, return deep link, authoritative status:
getStatus(session.sessionRef) on the return link and on app foreground; the status endpoint stays the only authoritative receipt.
TODO: untested — device behaviors in this section need real iOS/Android verification before go-live (see the Readiness Checklist); simulations cannot prove them.
Scheme rules
- Letter-first per RFC 3986 (
^[a-zA-Z][a-zA-Z0-9+.-]*:).createSessionrejects anything else withConfigErrorbefore the POST — digit-first schemes fail, and so does a value with no scheme prefix at all. Accepted values include custom schemes with or without//(mywallet://ramp,zingo:ramp) andhttps:URLs (universal/app links). Register the exact string — the server’s return-link allowlist compares the full value exactly: any added query string or fragment is rejected with 403 at session create. - Register and route the scheme in your own app (Linking config, intent filters). TODO: untested — confirm the exact scheme string your device build registers on iOS and Android; config-time validation can differ from runtime registration.
- Keep
zcash:(ZIP-321 payment URIs) registered separately and unchanged; it serves a different purpose. Do not reuse it as the ramp return scheme.
iOS notes
preferEphemeralSession: falsekeeps the user signed in to 0xramp.app across ramps; passkey autofill works.- iOS shows its website-data-sharing consent prompt each time the sheet opens — expected behavior, not a bug.
- The sheet is designed to close itself when the pane navigates to your return scheme; the deployed pane does not yet perform this navigation (see Pane readiness above).
- TODO: untested — verify passkey create/assert inside the auth-session browser on real devices, including returning-user login and the consent prompt, before rollout.
Stage 1 — SELL without any bridge (planned)
Stage 1 describes the intended pane behavior once pane-side support ships. It is not yet available in the deployed pane (see Pane readiness above):- The pane will show the transparent deposit address and the exact amount (display, QR, copy). The user sends from their wallet — your app is not in the signing path at all in this stage.
- The pane will detect the deposit on-chain and the flow continues.
- At flow end the pane will navigate to your return scheme, the sheet closes, and your app refreshes status from the ticketed status endpoint — the authoritative receipt for the fiat leg.
Stage 2 — deep-link handoff (DRAFT)
A draft, unfrozen codec (parseZecSendHandoffUrl, buildZecSendResumeUrl) removes the copy/paste step: the pane emits a handoff link carrying the payment request, your wallet confirms natively and signs with its own wallet core, and the resume link carries advisory txid evidence back to the pane. The encoding may change and pane-side conformance is not claimed. Deep links are advisory by design — your wallet’s native confirmation sheet stays the approval gate, and the status endpoint stays authoritative. Do not build production flows on it before it freezes. See API Reference for the draft functions.
Objects to persist
Treat
sessionRef + statusTicket as bearer data: read-only status, nothing more.