Lock an off-ramp flow
Build a hosted 0Gate sell flow that keeps payout handling, terminal state, and support recovery server-owned.
Use an off-ramp flow when the customer wants to sell crypto and receive a payout through the hosted 0Gate experience. Keep the user in 0Gate for the hosted flow, and keep final state in your backend.
Keep payout details partner-safe
This guide covers the partner workflow: create the intent, open the hosted flow, and close your record from verified backend state. Do not rely on browser callbacks for payout completion or expose provider, treasury, or settlement-operation details in your customer UI.
Flow boundary
| Boundary | Owned by | What to store |
|---|---|---|
| Sell intent | Your app | Account id, requested source asset, fiat currency, amount intent, and your reference id. |
| Hosted flow | 0Gate | Customer-facing sell journey, compliance checks, quote confirmation, and payout UX. |
| Backend event | 0Gate to you | Signed terminal event with enough reference data to close your own record. |
| Support record | You | Attempt timeline, event ids, current state, and retry/reconciliation notes. |
Implementation shape
- Create a sell intent in your system.
- Create a server-side 0Gate session with the flow locked to off-ramp.
- Store the session id and show the hosted sell block or redirect.
- Move the browser to processing after hosted success.
- Close the sell intent only from verified backend state.
async function startOffRamp(input: OffRampIntent) {
const intent = await sellIntents.create({
accountId: input.accountId,
sourceAsset: input.sourceAsset,
fiatCurrency: input.fiatCurrency,
status: 'pending_session',
});
const session = await gateSessions.createForAttempt({
attemptId: intent.id,
flow: 'off_ramp',
userReference: intent.id,
idempotencyKey: `off-ramp:${intent.id}`,
});
await sellIntents.attachGateSession(intent.id, session.id);
return { intentId: intent.id, clientSecret: session.clientSecret };
}State model
| State | Meaning | Partner action |
|---|---|---|
| pending_session | Sell intent exists, hosted session is not attached yet. | Keep retry safe with one idempotency key. |
| requires_action | Hosted off-ramp flow is open. | Let the user continue in 0Gate. |
| processing | Hosted UX finished, backend terminal state is pending. | Show processing and wait for trusted state. |
| fulfilled | Verified terminal state closed the sell intent. | Update balances, receipts, or support timeline once. |
| failed | The sell flow failed. | Keep the reason user-safe and offer retry/support. |
| expired | The session timed out. | Let the user start a new attempt. |
| cancelled | The customer or server cancelled. | Preserve the audit trail; do not reuse the session. |
Payout-safe UX
| Screen | Recommended copy | Avoid |
|---|---|---|
| Before hosted flow | Confirm amount, asset, and destination summary if you collect them. | Claims about exact rail timing unless approved. |
| After callback | “We are confirming your sell order.” | “Paid out” before backend confirmation. |
| Failure | “This flow could not be completed. Try again or contact support.” | Provider errors, compliance rule names, treasury balances. |
| Support | Show attempt id, status, timestamp, and next action. | Full webhook payloads or internal settlement notes. |
Webhook and reconciliation
For sell flows, be stricter about duplicate handling because customers may refresh or contact support while payout state is still processing.
async function closeSellIntent(event: GateSessionEvent) {
const intent = await sellIntents.findByGateSession(event.data.id);
if (!intent) return;
await webhookEvents.insertOnceOrReturn(event.id);
if (event.type === 'gate_session.completed') {
await sellIntents.fulfillOnce(intent.id, {
sourceEventId: event.id,
completedAt: event.createdAt,
});
}
}Production checklist
- Lock
off_rampon the server and keep browser config secondary. - Do not expose rail, provider, treasury, or compliance internals in public UI.
- Make duplicate webhooks no-ops.
- Give support a trusted status view backed by your database.
- Treat reconciliation as recovery, not a polling-first architecture.
Related pages
Build a 0Gate payment flow
Use the same durable session, webhook, and reconciliation architecture.
Check payment and payout methods
Show only currently supported options before the hosted flow.
Embed a single flow with kit blocks
Lock the hosted widget to off-ramp.
Protect customer data
Keep logs and support surfaces safe.