# Welcome

UR is the financial infrastructure for the open economy: manage and move stablecoins and fiat from a single account.

This site documents the UR Platform, the account layer that wallets, developers, and financial platforms build on. Through one integration, you give your users real accounts, co-branded Mastercards, and global money movement, without building the infrastructure yourself. It is written for the engineering, design, and product teams who integrate with UR.

Looking for UR's retail or corporate banking products instead? Visit [UR Support](https://support.ur.app).

### Jump right in

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-type="content-ref"></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>What is UR?</strong></td><td>How the account layer works.</td><td></td><td><a href="/pages/j6LjDMCbVLRE4ypUiNXf">/pages/j6LjDMCbVLRE4ypUiNXf</a></td><td></td><td></td><td><a href="/pages/j6LjDMCbVLRE4ypUiNXf">/pages/j6LjDMCbVLRE4ypUiNXf</a></td></tr><tr><td><strong>Quickstart</strong></td><td>Get access, set up your environment, and start integrating.</td><td></td><td><a href="/pages/B0278tGqRjwyjFVtCDJQ">/pages/B0278tGqRjwyjFVtCDJQ</a></td><td></td><td></td><td><a href="/pages/B0278tGqRjwyjFVtCDJQ">/pages/B0278tGqRjwyjFVtCDJQ</a></td></tr><tr><td><strong>Choose your approach</strong></td><td>Compare Managed Custody Mode and External Wallet Access Mode.</td><td></td><td><a href="/pages/AUT71JN7GvTHl0Mb0b79">/pages/AUT71JN7GvTHl0Mb0b79</a></td><td></td><td></td><td><a href="/pages/AUT71JN7GvTHl0Mb0b79">/pages/AUT71JN7GvTHl0Mb0b79</a></td></tr></tbody></table>


# What is UR?

UR is the financial infrastructure for the open economy. The UR Platform is the account layer that wallets, developers, and financial platforms build on.

A wallet stores value. An account lets you live financially.

UR is the financial infrastructure for the open economy: manage and move stablecoins and fiat from a single account. The infrastructure to turn on-chain value into a real financial life (identity, compliance, fiat rails, card, multi-currency) does not exist inside wallets. Building it yourself means banking licenses, card issuing partnerships, and years of compliance work.

The UR Platform is the account layer. It gives wallets, developers, and financial platforms the ability to offer real accounts through one integration, without building the infrastructure themselves.

## What your users get

When you integrate UR, your users receive:

* **Swiss IBAN**: a personal Swiss IBAN account in their name
* **On-chain identity (**[**URID**](/concepts/urid)**)**: a verified, portable identity issued to every user or business that has been KYCed or KYBed respectively
* [**Tokenized deposit**](/concepts/tokenized-deposits) **currencies**: EUR, USD, CHF, CNH, SGD, JPY, HKD, each backed 1:1 by fiat reserves
* **Mastercard debit card**: co-branded with your platform
* **Global bank transfers**: SEPA Instant (EUR), SWIFT (multi-currency)
* **Crypto off-ramp**: convert USDC, USDT, USDe, or ETH to fiat
* **FX conversion**: atomic burn-and-mint between currencies

## The stack

The account layer is built in four layers, each on top of the last:

| Layer        | Traditional finance                     | UR: the account layer                                                                                 |
| ------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **Identity** | KYC/KYB paperwork                       | URID, an on-chain identity issued after verification. Portable, programmable, and verifiable.         |
| **Money**    | Fiat held in bank accounts              | Tokenized deposits on-chain, each unit backed 1:1 by real deposits. Seven currencies, all composable. |
| **Rules**    | Compliance enforced by operations teams | Smart contracts. Core banking is code, not process.                                                   |
| **Data**     | Records in siloed databases             | Core banking and settlement is done on-chain. Transaction records are verifiable publicly.            |

## Core concepts

* [URID](/concepts/urid): the on-chain identity that unlocks UR accounts
* [Tokenized deposits](/concepts/tokenized-deposits): 1:1 fiat-backed tokens on Mantle Network
* [UR Account](/concepts/ur-account): a Swiss IBAN account issued by UR's regulated entity, connected to a URID
* [On-chain ledger](/concepts/on-chain-ledger): every transaction recorded on Mantle Network

## Platforms

* Web app: [get.ur.app](https://get.ur.app)
* [iOS App Store](https://apps.apple.com/app/ur-fiat-crypto-unified/id6748420070)
* [Google Play](https://play.google.com/store/apps/details?id=com.mantle.ur)


# Partner integration flow concepts

Partner-facing concept overview. For API references and technical integration details, see docs.ur.app.

UR acts as the financial infrastructure layer for neobank partners. Partners integrate via a single REST API; UR handles KYC compliance, user account custody, and the underlying banking rails. Partners choose from a menu of modular services to build their product.

### How partners integrate with UR <a href="#bf47b023-f09f-4009-901e-ad6ba6b89123" id="bf47b023-f09f-4009-901e-ad6ba6b89123"></a>

Partners connect to UR as a financial infrastructure provider. UR sits between the partner's product and the global banking rails, managing identity, user accounts, and regulated financial services behind a clean API surface.

**Modular services partners can enable:**

* Multi-currency fiat balance (EUR, USD, CHF, SGD, HKD, JPY)
* Fiat Pay-in via bank transfer
* Fiat Payout via bank transfer
* Card spending via co-branded Mastercard debit

```mermaid
flowchart TD
    User(["End User"]) -->|Uses| App

    subgraph Partner["Partner Platform"]
        App["Partner App & UX"]
        Backend["Partner Backend"]
        App --> Backend
    end

    Backend -->|REST API| UR

    subgraph UR["UR: Financial Infrastructure"]
        KYC["KYC & Compliance\nAll users verified by UR\nPartners may assist data collection"]

        subgraph Services["Modular Financial Services  (partner selects)"]
            direction LR
            Bal["Multi-Currency\nFiat Balance"]
            PayIn["Fiat Pay-in\n(Bank Transfer)"]
            PayOut["Fiat Payout\n(Bank Transfer)"]
            Cards["Card Spending\n(Co-branded Mastercard)"]
        end

        KYC -->|"User verified: Live"| Services
    end

    subgraph Rails["Banking Rails  (managed by UR)"]
        direction LR
        SEPA["SEPA · SWIFT\nGlobal Transfers"]
        MC["Mastercard Network\nGlobal Acceptance"]
    end

    PayIn <--> SEPA
    PayOut <--> SEPA
    Cards <--> MC
```

**Reference docs:**

* [Fiat Pay-in (Bank Transfer)](https://docs.ur.app/concepts/deposits)
* [Fiat Payout (Bank Transfer)](https://docs.ur.app/concepts/withdrawals)
* [Core Banking Overview](https://docs.ur.app/concepts/core-banking-overview)

### Card spending: Buffer Pool model <a href="#id-21f9d20d-0dc9-41fb-a611-7c47f51c662c" id="id-21f9d20d-0dc9-41fb-a611-7c47f51c662c"></a>

> Suited for partners who hold significant crypto assets on behalf of their users (e.g. a crypto exchange or a neobank).

In this model, the partner pre-funds a fiat/USDC-based buffer pool held at UR. Card spending draws from this pool instantly; no per-user offramp is required at the moment of spend. The pool is replenished periodically by the partner, entirely invisible to end users.

**Phase 1: Prefund (partner-initiated)**

The partner periodically tops up their buffer pool at UR. UR maintains a minimum balance to ensure continuous card authorization capacity.

**Phase 2: Card Spend (user-initiated)**

When a user taps their card, UR routes the authorization and checks with the partner in real time. Upon approval, the spend is drawn from the buffer pool and routed through the user's linked card account.

**User experience:** Seamless; the user taps and pays. No manual conversion step required.

```mermaid
sequenceDiagram
    participant Partner as Partner Platform
    participant UR as UR
    participant Pool as Partner Buffer Pool<br/>(held at UR)
    participant MC as Mastercard Network
    participant Merchant

    Note over Partner, Pool: Phase 1: Prefund (automated · daily / weekly)
    Partner->>UR: Sends USDC to UR buffer pool
    UR-->>Pool: Credit fiat to Partner's buffer account (ops account)
    Note over Pool: Minimum balance maintained<br/>for card spending capacity

    Note over Partner, Merchant: Phase 2: Card Spending (user-initiated)
    Merchant->>MC: Authorization request (user taps card)
    MC->>UR: Route: Authorize €X for this card?
    UR->>Partner: Webhook: "Approve spend for this user?"
    Partner-->>UR: Approved
    UR->>Pool: Debit from Partner's buffer pool
    Pool-->>UR: Confirmed
    UR->>UR: Fund user's linked card account
    UR-->>MC: Approved
    MC-->>Merchant: Transaction complete
```

### Card spending: off-ramp first <a href="#id-31f192a8-f469-4487-8b36-5e6b3db6aa35" id="id-31f192a8-f469-4487-8b36-5e6b3db6aa35"></a>

> Suited for partners where users hold their own crypto in self-custody, external wallets or for users wanting to spend non-stablecoin assets.

In this model, users must first convert their crypto to fiat before spending. Once fiat lands in their UR account, the card draws from that balance. No partner-side buffer pool is required.

**Step 1: Offramp (user-initiated)**

The user converts crypto to fiat via the partner app. UR executes the conversion at a quoted rate and credits the user's UR fiat balance.

**Step 2: Card spend**

The user taps their card. UR authorizes against the available fiat balance.

**User experience:** Users need to convert before they can spend.

```mermaid
sequenceDiagram
    participant User as End User
    participant Partner as Partner Platform
    participant UR as UR
    participant MC as Mastercard Network
    participant Merchant

    Note over User, UR: Step 1: Convert crypto to fiat (user-initiated)
    User->>Partner: Initiate cash-out (e.g. 500 USDC)
    Partner->>UR: Request conversion quote (USDC → EUR)
    UR-->>Partner: Quote returned (e.g. €431 · valid 30s)
    User->>Partner: Confirm
    Partner->>UR: Execute conversion
    UR-->>User: €431 credited to UR fiat balance

    Note over User, Merchant: Step 2: Card spending (draws from fiat balance)
    User->>Merchant: Tap UR co-branded card
    Merchant->>MC: Authorization request
    MC->>UR: Route: Authorize €50?
    UR->>UR: Verify user fiat balance
    UR-->>MC: Approved
    MC-->>Merchant: Transaction complete
```

**Reference docs:**

* [Crypto-to-Fiat Offramp flow](https://docs.ur.app/concepts/deposits#crypto-to-fiat-conversion-offramp)
* [Card Spending flow](https://docs.ur.app/concepts/what-is-ur#the-spending-flow-from-fiat-balance-to-a-real-world-purchase)
* [External Wallet Access Mode](/integration-methods/external-wallet-access-mode)


# Quickstart

Get API credentials and start integrating with UR in minutes.

## Get access

The fastest way to start is the [API sandbox](https://partner.ur.app/api-sandbox). On your first sign-in it creates your `partnerId` and signing key, so you can begin calling UR APIs without waiting on UR. Store your private key; UR does not keep it. See [API signing key](/getting-started/integration-guide#api-signing-key) for this path and for bringing your own EVM address instead.

You also get a dedicated integration channel with the UR team for anything the sandbox does not cover, such as production configuration. Email **<support@ur.app>** to set it up.

{% hint style="info" %}
Creating your sandbox credentials is self-serve. Moving a test account to `Live` status still runs through the UR team, because testnet KYC results are mocked.
{% endhint %}

## Access the sandbox environment

UR provides a sandbox environment for integration development and testing. You generate your own credentials in the [API sandbox](https://partner.ur.app/api-sandbox); the UR team promotes test accounts to `Live` status, because testnet KYC results are mocked.

See [Sandbox environment](/getting-started/sandbox-environment) for the base URL and testnet notes.

## Authenticate your API requests

All API requests are authenticated using EIP-191 signatures. You'll sign requests with your partner wallet, and UR verifies the signature on every call.

See [Signature and Verify](https://docs.ur.app/api-reference/signature-and-verify) for how it works, including code examples.

## Next steps

* [Choose your approach](/getting-started/integration-guide): compare Managed Custody Mode and External Wallet Access Mode
* [OpenAPIs](https://docs.ur.app/api-reference): understand the API surfaces


# Choose your integration options

Integrating with UR comes down to three independent decisions. Pick one option from each.

UR is modular. You build your integration from three independent decisions: how your platform connects (Account Mode), how card spend settles (Card Mode), and how users verify identity (KYC Mode). Each decision stands on its own, so any combination of options works.

## Three decisions

Before integration kicks off, confirm one option from each:

| Decision                          | Options                                                                      | What it determines                                          |
| --------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------- |
| [**Account Mode**](#account-mode) | Managed Custody Mode · External Wallet Access Mode (+ legacy Delegated Mode) | How your platform connects to UR and who signs transactions |
| [**Card Mode**](#card-mode)       | Fiat Only · Crypto Backed · No card                                          | Where card spend draws funds from                           |
| [**KYC Mode**](#kyc-mode)         | UR-hosted webview · Sumsub SDK in your app · Sumsub reuse (share token)      | How the user completes identity verification                |

Once these three are fixed, the rest of the integration is a standard menu of Core Banking APIs (Pay-in, Payout, On-ramp (coming soon), Off-ramp, FX, Card) wired against the chosen options.

{% hint style="info" %}
Regardless of which options you choose, your platform is responsible for mapping and maintaining the relationship between your users' identifiers and their corresponding URID. This allows your platform to accurately reflect user states based on UR APIs and webhooks.
{% endhint %}

## Before you start

Every integration starts with the same setup, whichever options you pick. Work through the following steps with UR once:

{% stepper %}
{% step %}

### Contact UR

Contact UR at <support@ur.app>. UR opens your dedicated integration channel, runs due diligence, and starts your onboarding.
{% endstep %}

{% step %}

### Confirm your integration options

Confirm your integration options with UR: your Account Mode, your Card Mode, your KYC Mode, and the services you want.
{% endstep %}

{% step %}

### Check eligibility and limits

Two rules are enforced by UR, not configured by you, and both shape your product before you write any code:

* **Eligibility.** UR checks country and nationality rules when a user opens an account. A user outside the supported lists cannot onboard, no matter how your funnel is built, so design for the rejection path. See [Supported regions](/getting-started/supported-regions).
* **Monthly limit.** Every `Live` user carries a rolling monthly limit, denominated in CHF. Outbound and conversion operations (card spend, FX, payout, on-ramp) are checked against it and fail once it is exhausted. Read each user's live limit rather than hardcoding a figure, and subscribe to the `monthly_limit_exceeded` webhook so you can surface it. See [Fetch UR Account information](https://docs.ur.app/api-reference#fetch-ur-account-information) and [Webhooks](/developer-resources/webhook).

Raising a user's limit is a compliance review with UR, not an API call.
{% endstep %}

{% step %}

### Generate and register your signing key

Get your signing key and `partnerId`. You have two options: create them in the [API sandbox](https://partner.ur.app/api-sandbox), which generates them for you, or generate an EVM key pair yourself and send UR the public address. Either way, UR issues a `partnerId` that scopes your API calls, and your private key stays on your side. See [API signing key](#api-signing-key) on this page for both paths and the full key lifecycle.
{% endstep %}

{% step %}

### Register your webhook URL

Register your webhook URL with UR so you receive events.
{% endstep %}

{% step %}

### Request sandbox access

Request sandbox access from the UR team. Integrate against sandbox before production.
{% endstep %}

{% step %}

### Go live

Confirm your production configuration with UR, then go live.
{% endstep %}
{% endstepper %}

## Account Mode

Account Mode determines how your platform connects to UR and who controls transaction signing.

### Comparison

| Feature            | Managed Custody Mode                                                                        | External Wallet Access Mode                              | Delegated Mode                                   |
| ------------------ | ------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------ |
| Integration style  | Web2 / API-first                                                                            | Web3 / smart contract-first                              | Web2 / API-first                                 |
| User onboarding    | Partner-managed (Sumsub SDK or UR Webview)                                                  | Partner implements all UIs                               | Redirect user to UR Webview                      |
| Account creation   | API-driven account provisioning                                                             | User signs a message with their wallet to mint a URID    | Redirect to UR Webview                           |
| Transactions       | Partner signs for fiat actions; user signs only for crypto (off-ramp)                       | User signs every action via their wallet                 | API-driven via delegated contract allowances     |
| Asset custody      | Fiat: UR-managed account / Crypto: external (non-UR) wallet, partner-side or the user's own | Self-custody in user's single external wallet            | TEE-based embedded wallet (e.g., Turnkey, Privy) |
| Gas fees           | UR pays (operator wallets)                                                                  | User pays (gasless option available)                     | UR pays (operator wallets)                       |
| Card management    | API-driven                                                                                  | User signature to authorize card creation and settings   | Redirect to UR Card Webview                      |
| Development effort | Lower (standard REST API integration)                                                       | Higher (requires Web3 signatures and wallet connections) | Lower (standard REST API integration)            |

{% hint style="warning" %}
Delegated Mode is a legacy integration. New partners should use Managed Custody Mode or External Wallet Access Mode. See [Delegated Mode](/integration-methods/delegated-mode) for existing partners.
{% endhint %}

### When to use each mode

We recommend **Managed Custody Mode** if your users don't have crypto wallets today, or if you want a banking app experience without exposing blockchain mechanics. Your backend calls UR APIs for fiat actions. The user's crypto lives in an external (non-UR) wallet, partner-side or the user's own; the UR-managed account holds fiat only. The user never deals with a wallet for banking actions.

We recommend **External Wallet Access Mode** if your users already manage their own wallets (MetaMask, Rabby, etc.) and expect to sign transactions themselves. You get more control over timing and execution, but you need Web3 infrastructure on your side.

| Your platform looks like...                | We recommend                |
| ------------------------------------------ | --------------------------- |
| Consumer FinTech app, exchange, or neobank | Managed Custody Mode        |
| DApp, Web3 wallet, or DeFi aggregator      | External Wallet Access Mode |

### Managed Custody Mode

{% hint style="info" %}
**Recommended path.** Managed Custody Mode requires the least development effort (standard REST APIs, no Web3 wallet handling). Your backend orchestrates the user's fiat account via UR API; the user's crypto sits in an external (non-UR) wallet, partner-side (managed by your backend) or the user's own. The UR-managed account holds fiat only.
{% endhint %}

Your backend orchestrates the user's fiat account (managed by UR). The user's crypto sits in an external (non-UR) wallet, partner-side or the user's own; the UR-managed account holds fiat only. Users never interact with a wallet for fiat actions.

#### Architecture

```mermaid
flowchart LR

subgraph Partner["Partner Platform"]
PB["Partner Backend"]
end

subgraph UR["UR"]
API["UR API"]
FIAT["User Fiat Account
(UR-managed)
Tokenized deposits, IBAN, Card"]
end

CRYPTO["User Crypto
(external non-UR wallet:
partner-side or user's own)"]

subgraph Settlement["Settlement"]
CHAIN["On-chain Settlement"]
end

PB -->|"API calls\n(partner signs)"| API
API --> FIAT
PB -.->|"Direct management\n(when the wallet is partner-side)"| CRYPTO
FIAT --> CHAIN
CRYPTO --> CHAIN
```

#### How it works

* Your backend calls UR API for all fiat actions (pay-in, payout, FX, on-ramp (coming soon), card).
* UR validates authentication, compliance, and executes on-chain.
* For off-ramp, your backend fetches a quote from UR; crypto is submitted to the UR off-ramp contract from the external (non-UR) wallet that holds it (partner-side or the user's own).
* UR notifies you via webhooks on settlement.

#### User experience

Users complete KYC once during onboarding. After that, all banking actions (fiat and off-ramp) are handled by the partner backend; users are not prompted to sign.

See [Managed Custody Mode](/integration-methods/managed-custody-mode) for detailed integration steps and the [API reference](https://docs.ur.app/api-reference/account/managed-custody-mode) for endpoints.

#### Integration SOP

Beyond the common setup in [Before you start](#before-you-start), Managed Custody Mode adds the following checklist:

* Register your mainnet signing key through your integration channel so your backend can make API calls.
* Implement account access authorization in your user onboarding. It has two mandatory parts: an in-app authorization confirmation the user accepts before you enable any banking function, and a confirmation email you send with `legal@ur.app` on BCC. The BCC is mandatory; it is UR's compliance record. See [account access authorization](/integration-methods/managed-custody-mode#account-access-authorization).
* Read the [Managed Custody Mode API reference](https://docs.ur.app/api-reference/account/managed-custody-mode) for account creation, profile, pay-in details, payout, FX, off-ramp quote, and card.
* Subscribe to the settlement and status webhooks.
* Map each user ID in your system to the URID issued at user onboarding.

### External Wallet Access Mode

External Wallet Access Mode is for platforms where users bring their own wallet (e.g., MetaMask, Rabby) and maintain full self-custody of their keys.

{% hint style="info" %}
UR does not provision an additional wallet for these users. The URID and fiat balances (tokenized deposits) are managed directly within the user's external wallet.
{% endhint %}

#### Architecture

```mermaid
flowchart LR

subgraph PartnerScope["Partner Platform"]
PFE["Partner Frontend / DApp"]
PBE["Partner Backend"]
end

subgraph UserScope["User"]
ActorUser(("User"))
UW["External Wallet"]
end

subgraph URScope["UR Backend"]
API["Partner API"]
VAL["Compliance / Risk<br/>& Quote Service"]
WH["Event Listener /<br/>Webhooks"]
end

subgraph ContractScope["UR Smart Contracts"]
SC["Token / Deposit /<br/>Router Contracts"]
CHAIN["Fiat Balance / Assets"]
end

%% Flow Logic
ActorUser -- Controls --> UW
UW -- Connects to --> PFE

%% 1. Preparation
PFE -- "1. Request Action" --> PBE
PBE -- "2. Get Quote/Params" --> API
API --> VAL
VAL -- "3. Return Tx Data" --> PBE
PBE -- "4. Pass Data" --> PFE

%% 2. Execution (The key difference: User signs & submits)
PFE -- "5. Request Signature" --> UW
UW -- "6. Sign & Submit Tx" --> SC

%% 3. Settlement
SC --> CHAIN

%% 4. Notification
SC -.-> WH
WH -. "7. Notify Status" .-> PBE
```

#### Request flow

* Partner frontend requests an action from the partner backend.
* Partner backend calls UR API to get quote or transaction parameters.
* UR returns transaction data.
* Partner frontend prompts the user to sign with their wallet.
* User signs and submits the transaction to UR smart contracts.
* UR listens for on-chain events and notifies the partner via webhook.

#### User experience

The experience is Web3-native. Users must connect their wallet and sign messages or transactions to approve actions. Your platform acts as a facilitator, relaying these signatures to UR or guiding the user to interact with smart contracts directly.

See [External Wallet Access Mode](/integration-methods/external-wallet-access-mode) for detailed integration steps.

#### Integration SOP

In this mode the URID and the user's fiat balances live in the user's own wallet, and the user signs their own transactions. Fiat transactions settle on Mantle Network, so make sure your users' wallets can interact with Mantle.

Beyond the common setup in [Before you start](#before-you-start), the mode-specific steps are the following:

* Register your mainnet signing key through your integration channel so your backend can make API calls.
* Read the [External Wallet Access Mode API reference](https://docs.ur.app/api-reference/account/external-wallet-access-mode).

## Card Mode

Card Mode determines where card spend draws funds from when your user taps their co-branded debit card. You choose a Card Mode separately from your Account Mode.

### Fiat Only

> The card draws exclusively from the user's UR fiat balance.

Card spend is booked against the user's existing UR fiat balance. The user funds that balance through any of the standard channels: bank pay-in, crypto off-ramp into UR, or transfers from another UR user. No partner-side prefund pool is required.

If the user wants to spend crypto, they first **off-ramp** crypto into their UR fiat balance, then tap the card. The off-ramp and the card swipe are two separate operations.

**Step 1 (optional): Off-ramp, user-initiated, only if the user is starting from crypto**

The user converts crypto to fiat. UR executes the conversion at a quoted rate and credits the user's UR fiat balance.

**Step 2: Card swipe**

The user taps their card. UR authorizes against the available UR fiat balance.

**User experience:** if the user already holds fiat in UR, the swipe is one tap. If they're starting from crypto, they need to off-ramp first.

```mermaid
sequenceDiagram
    participant User as End User
    participant Partner as Partner Platform
    participant UR as UR
    participant MC as Mastercard Network
    participant Merchant

    Note over User, UR: Step 1 (optional): Convert crypto to fiat
    User->>Partner: Initiate off-ramp (e.g. 500 USDC)
    Partner->>UR: Request conversion quote (USDC → EUR)
    UR-->>Partner: Quote returned (e.g. €431 · valid 30s)
    User->>Partner: Confirm
    Partner->>UR: Execute conversion
    UR-->>User: €431 credited to UR fiat balance

    Note over User, Merchant: Step 2: Card spending (draws from UR fiat balance)
    User->>Merchant: Tap UR co-branded card
    Merchant->>MC: Authorization request
    MC->>UR: Route: Authorize €50?
    UR->>UR: Verify user fiat balance
    UR-->>MC: Approved
    MC-->>Merchant: Transaction complete
```

**API reference:** No additional partner-side integration surface. UR handles authorization on-chain against the user's tokenized fiat balance. Use the standard Card endpoints in your Account Mode reference ([External Wallet Access Mode](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-3-card) or [Managed Custody Mode](https://docs.ur.app/api-reference/account/managed-custody-mode#id-11-card)).

#### Integration SOP

Fiat Only is the lightest card option to set up. The setup is the following checklist:

* Complete the [co-branded card artwork](#co-branded-card-artwork) submission if you brand the card.
* Use the standard Card endpoints in your Account Mode API reference: create card, get card info, set default currency, and card history. There is no card-specific settlement surface to build. See the card sections for [Managed Custody Mode](https://docs.ur.app/api-reference/account/managed-custody-mode#id-11-card) and [External Wallet Access Mode](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-3-card).

### Crypto Backed

> The card can settle directly against the user's crypto holdings, with no per-swipe off-ramp required.

This mode is for products that need users to spend crypto directly via the card, without manually off-ramping into fiat before each purchase. You do not have to custody the user's crypto for this to work. What matters is that you can reliably debit the user's crypto after a swipe is approved, through centralized custody, a smart contract wallet under your programmatic control, or an equivalent arrangement.

The mechanism is a **Buffer Pool** (also called the *Prefund channel*): you keep your Prefund Account topped up by off-ramping USDC into it, and card authorizations settle in real time from that prefunded balance. You then debit the user's crypto asynchronously after the swipe, using UR's swipe-result webhook as the trigger. UR only reports the fiat swipe amount; you decide, based on your own pricing and conversion logic, how much crypto to debit from the user.

**Phase 1: Prefund (partner-initiated, scheduled)**

You call the UR off-ramp contract on a supported off-ramp chain, converting USDC into your Prefund Account; the Prefund Account is the target account of the off-ramp. Schedule recurring off-ramp calls in your backend to keep the account above the minimum balance agreed with UR, so card authorization capacity stays continuous.

**Phase 2: Card spend (user-initiated, real-time)**

When the user taps their card, UR routes the authorization and asks you via webhook whether to approve, and which source to use. You must respond within **500 ms** so UR can return an `APPROVE` / `DECLINE` to Mastercard within its **1-second** total authorization window; your 500 ms sits inside that 1 second.

Your response picks the funding source:

* Settle against your Prefund Account, and debit the user's crypto yourself after the swipe.
* Settle against the user's UR fiat balance, the same path as a Fiat Only card.

The exact field contract for the authorization callback lives in the [Card Mode: Crypto Backed API reference](https://docs.ur.app/api-reference/cards/crypto-backed-card).

After UR returns the result to Mastercard, UR sends you a follow-up webhook. On a successful `CRYPTO` swipe, **you are responsible** for debiting the equivalent crypto from the user. UR does not move crypto on your behalf. How you enforce the debit is part of your own business logic, and typically relies on a smart contract wallet under your programmatic control or a centralized custody arrangement so the user cannot move the crypto between approval and debit.

**User experience:** Seamless. The user taps and pays. No manual conversion step is required.

**Refund handling:** Any card refund is credited to the user's UR fiat balance, regardless of whether the original spend was settled from digital assets or fiat.

```mermaid
sequenceDiagram
    participant Partner as Partner Platform
    participant ORC as UR Off-ramp Contract
    participant UR as UR
    participant Pool as Prefund Account<br/>(UR Managed Fiat Account)
    participant MC as Mastercard Network
    participant Merchant

    Note over Partner, Pool: Phase 1: Prefund (scheduled, recurring)
    Partner->>ORC: Call off-ramp contract with USDC
    ORC-->>Pool: Credit Prefund Account
    Note over Pool: Minimum balance maintained<br/>for card spending capacity

    Note over Partner, Merchant: Phase 2: Card spending (user-initiated)
    Merchant->>MC: Authorization request (user taps card)
    MC->>UR: Route: Authorize €X?
    UR->>Partner: Webhook: "Approve? CRYPTO or FIAT?"<br/>(respond within 500 ms)
    Partner-->>UR: APPROVE (settle from Prefund Account)
    UR->>Pool: Book spend against Prefund Account
    UR-->>MC: Approved (within 1 s window)
    MC-->>Merchant: Transaction complete
    UR->>Partner: Webhook: authorization.result
    Partner->>Partner: Debit equivalent crypto from user's wallet
```

**Prerequisites for Crypto Backed**

* Ability to make an initial USDC prefund to seed card spending capacity.
* Ability to schedule recurring off-ramp calls that top up your Prefund Account on a supported off-ramp chain.
* Ability to respond to the authorization webhook within 500 ms.
* Ability to reliably debit crypto from the user **after** the swipe is approved (typically via a smart contract wallet under your programmatic control or centralized custody, so the user cannot move funds between approval and debit).
* Operational tolerance for working-float management. If your pool drains below the minimum balance, authorizations begin to decline.

**API reference:** [Card Mode: Crypto Backed](https://docs.ur.app/api-reference/cards/crypto-backed-card) plugs into either Account Mode. Three integration surfaces: [Prefund account](https://docs.ur.app/api-reference/cards/crypto-backed-card#id-3-prefund-account), [Card authorization callback](https://docs.ur.app/api-reference/cards/crypto-backed-card#id-4-card-authorization-callback-ur-greater-than-partner), [Card Mode webhooks](https://docs.ur.app/api-reference/cards/crypto-backed-card#id-5-card-mode-webhooks).

#### Integration SOP

Crypto Backed takes the most setup of the three card options, and the order matters. Work through the following steps:

{% stepper %}
{% step %}

### Request Crypto Backed

Tell UR through your dedicated integration channel that you want Card Mode: Crypto Backed. There is no API to enable it.
{% endstep %}

{% step %}

### Complete KYB

Complete KYB (Know Your Business) with UR. UR requires KYB before it provisions your Prefund Account.
{% endstep %}

{% step %}

### UR provisions your Prefund Account

UR provisions your [Prefund Account](https://docs.ur.app/api-reference/cards/crypto-backed-card#id-3-prefund-account). It is one account per partner, and it can hold a balance in more than one settlement currency.
{% endstep %}

{% step %}

### Agree your settlement currencies

Tell UR which settlement currencies your Prefund Account should hold. It defaults to USD. Your settlement currency affects your off-ramp pricing, so agree it before onboarding rather than after you go live.
{% endstep %}

{% step %}

### Choose your settlement mode

Your settlement mode decides who picks the debit currency on each swipe, and whether UR can pre-compute the amount for you:

* **Fixed** (the default) means UR pre-computes the exact amount it will debit, including FX and interchange, and sends it in the authorization request. You approve or decline against that amount and run no conversion. You do not pick the debit currency per swipe.
* **Partner controlled** means you pick which of your settlement currencies to debit on each swipe. UR cannot pre-compute the amount, because it does not know your choice in advance, so you size the debit yourself.

Start with Fixed unless you need per-swipe currency control. See the [settlement modes reference](https://docs.ur.app/api-reference/cards/crypto-backed-card#id-4.1-settlement-modes).
{% endstep %}

{% step %}

### Agree the funding parameters and minimum balances

Agree the off-ramp chain and the deposit currency (USDC only today) with UR.

Agree a **minimum balance for each settlement currency**. Thresholds are per currency, not per account, and UR emits a `prefund.balance.alert` when a currency reaches or falls below its threshold. Set each one above the float you need to absorb a normal day of swipes; when a currency runs dry, authorizations in that currency start to decline.
{% endstep %}

{% step %}

### Receive your Prefund Account details

UR gives you your Prefund Account details. You use the Prefund Account as the target account when you off-ramp.
{% endstep %}

{% step %}

### Register your card authorization callback URL

Register your [card authorization callback](https://docs.ur.app/api-reference/cards/crypto-backed-card#id-4-card-authorization-callback-ur-greater-than-partner) URL with UR. UR calls it on every card authorization. Changing the URL later is coordinated with UR and paired with signature validation; there is no self-serve endpoint.
{% endstep %}

{% step %}

### Seed and maintain the Prefund Account

At mainnet launch, call the off-ramp contract to convert USDC into your Prefund Account and seed card spending capacity. Schedule recurring off-ramps in your backend to keep the Prefund Account topped up.
{% endstep %}
{% endstepper %}

### No card

Choose No card if your platform does not issue cards. Every other service in your integration (pay-in, payout, FX, off-ramp) works the same without one.

#### Integration SOP

There is nothing to set up and nothing to build. The Card endpoints, the card authorization callback, and the card artwork submission do not apply.

## KYC Mode

UR is a regulated financial product. Every user must complete KYC (Know Your Customer) checks before accessing core banking features. You choose *how* KYC is delivered to your users. The right option depends on whether you already verify users with Sumsub today.

For the full list of what UR verifies and the data UR collects, see [KYC & Compliance](/concepts/kyc-and-compliance). That page covers identity methods, including NFC, penny transfer, and video verification, plus domicile checks, questionnaire categories, and AML/sanctions screening. This section focuses on integration, not data collection.

### Choosing a method

Identity verification uses an NFC scan, so the user needs an NFC-capable mobile device; on the UR-hosted webview, UR runs that scan for you.

```mermaid
flowchart TD
  Start([Choosing how KYC runs])
  Q1{Already verify users in<br/>your own Sumsub tenant?}
  M3[Sumsub reuse<br/>share token]
  Q2{Run KYC inside<br/>your own app?}
  M1[UR-hosted webview]
  M2[Sumsub SDK in your app]

  Start --> Q1
  Q1 -- Yes --> M3
  Q1 -- No --> Q2
  Q2 -- No --> M1
  Q2 -- Yes --> M2

  style M3 fill:#f0fff4,stroke:#38a169
```

**Which method fits you**

* **UR-hosted webview** if you have no KYC vendor and no mobile app of your own. UR runs the identity scan for you inside its webview.
* **Sumsub SDK in your app** if you want KYC inside your own app and you have a mobile app to run the identity scan.
* **Sumsub reuse (share token)** if you already verify your users in your own Sumsub tenant.

**Upsides and downsides**

| Method                                                                    | Upsides                                                                                                                                                 | Downsides                                                                                                                          |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| **UR-hosted webview**                                                     | Lowest effort; no KYC vendor of your own. UR runs the identity scan for you.                                                                            | The user leaves your app for UR's webview, so you have less control over the experience. KYC data lands in UR's Sumsub, not yours. |
| **Sumsub SDK in your app**                                                | KYC feels native in your app. Minimal setup: integrate the Sumsub SDK and run UR's verification workflow through it, with no Sumsub tenant of your own. | KYC data lands in UR's Sumsub, not yours.                                                                                          |
| **Sumsub reuse (share token)** *(recommended if Sumsub already in place)* | No re-verification for users you have already verified. The verification data already lives in your own Sumsub tenant.                                  | You run your own Sumsub tenant and automate the share-token handoff. Works only for users you have already verified.               |

**Where the KYC data lands**

* **UR-hosted webview** and **Sumsub SDK in your app**: the verification is created in UR's Sumsub tenant.
* **Sumsub reuse (share token)**: the data already lives in your own Sumsub tenant; you share it to UR.

### UR-hosted webview

Your platform redirects the user to a UR-hosted webview, where the full KYC flow runs end to end. You need no KYC vendor of your own; UR delivers outcomes via the [`kyc_status` webhook](/developer-resources/webhook).

**Happy path**

* You redirect the user to UR's webview after partner-side authentication.
* The user completes the questionnaire, the domicile (device location) check, and Form A signing in the UR webview.
* For identity verification, the user completes the passport/national-ID NFC scan, the document scan, and the selfie in **ReadID**, opened as the ReadID app on Android or as an [App Clip](https://developer.apple.com/app-clips/) on iOS. UR orchestrates this handoff for you.
* UR notifies you via the `kyc_status` webhook when KYC reaches `Live`.

**When to use it:** Choose this path if you have no KYC vendor, or no mobile app of your own. You do not build the NFC scan; UR orchestrates the identity-verification handoff to ReadID for you. The user still completes the scan in ReadID on an NFC-capable mobile device.

#### Integration SOP

This method has no off-code setup beyond the common steps in [Before you start](#before-you-start). Build the redirect and consume the `kyc_status` webhook; there is nothing extra to request, sign, or configure.

### Sumsub SDK in your app

You integrate the Sumsub SDK in your app and run UR's verification workflow through it; you do not run a Sumsub tenant of your own. UR's backend issues the token that starts the SDK. Setup is minimal: the SDK integration plus one backend call.

**Happy path**

* Your backend calls `POST /api/v1/sumsub/create-access-token` for the user. See [Section 2.1.5 in the API reference](https://docs.ur.app/api-reference/account/external-wallet-access-mode).
* Your frontend initializes the Sumsub SDK with the returned `token`.
* User completes KYC inside the partner app, including questionnaire, identity verification, domicile check, and Form A.
* UR notifies the partner via the `kyc_status` webhook on completion.

The NFC scan runs in the Sumsub mobile SDK, so this method needs a mobile app. If your platform is web only, use the **UR-hosted webview** instead, where UR runs the NFC scan for you. Penny transfer is an alternative for users who cannot complete an NFC scan (FATF jurisdictions); see [KYC & compliance](/concepts/kyc-and-compliance).

**When to use it:** You want KYC to feel native inside your app, you have a mobile app for the NFC scan, and you have the engineering capacity to integrate Sumsub.

#### Integration SOP

This method has no off-code setup beyond the common steps in [Before you start](#before-you-start). Integrate the Sumsub SDK, call the token endpoint, and consume the `kyc_status` webhook; there is nothing extra to request, sign, or configure.

### Sumsub reuse (share token) *(recommended if already on Sumsub)*

If you have already verified the user with Sumsub on your own tenant, you can share that verification with UR through Sumsub's **share-token** mechanism. UR reruns the required checks against UR's verification level. In the happy path, the user is not prompted again.

**Prerequisites**

* Your platform uses Sumsub.
* You and UR have configured each other as **Donor / Recipient Partners** in the Sumsub dashboard.
* A Data Processing Agreement (DPA) is in place between your company and UR.
* Your KYC flow presents the [required data-sharing declaration](#kyc-data-sharing-disclosure) to the user and captures their agreement before identity verification. Your privacy notice also discloses the share to UR.

{% hint style="warning" %}
**Required KYC disclosure for data sharing.** This option shares the user's verified KYC profile with UR through Sumsub's Reusable KYC network. This is a KYC requirement: your KYC flow must present the following declaration to the user and capture their agreement before they start identity verification. Surface it in your own KYC steps; UR does not show it for you on this path. Use this exact wording:

> By proceeding with identity verification, I acknowledge and agree that my KYC information and verification results may be shared by us and/or Sumsub with Participating Verification Partners within Sumsub's Reusable KYC network for identity verification, anti-money laundering compliance, sanctions screening, regulatory compliance, fraud prevention, and other legal or regulatory purposes, including compliance requirements applicable to such Participating Verification Partners that may be independent from the specific products or services offered on our platform. For the purpose of this declaration, "Participating Verification Partners" means any third-party that participates in Sumsub's Reusable KYC network.

This declaration is the user-facing basis for the Sumsub share-token reuse below. It complements the Data Processing Agreement (DPA), which governs the share between your company and UR.
{% endhint %}

**Happy path**

* Your backend listens for Sumsub's `applicantReviewed` webhook on your tenant.
* On approval, your backend calls Sumsub to generate a **share token** scoped to UR's `clientId`.
* Your backend submits the share token together with your internal user reference to UR through the channel agreed with UR integration support.
* UR calls Sumsub `reuse` against UR's verification level. Selfie/liveness is reused from the donor profile.
* UR notifies you via the `kyc_status` webhook with the result.

**When to use it:** Your users have already been verified on your Sumsub tenant and you want to onboard them to UR without re-verification.

**What this option is not:** UR does not accept raw KYC data exports through your app token. The share-token reuse flow is the only supported backend path because it produces a verification attestation on UR's Sumsub tenant, which is required for UR's Customer Due Diligence (CDD) obligations.

#### Integration SOP

This method shares verification data between two companies, so it carries real off-code prerequisites. Complete the following before the first user flows through:

* Coordinate the setup with UR through your dedicated integration channel.
* Run your own Sumsub tenant.
* Configure UR and your platform as Donor / Recipient Partners in the Sumsub dashboard.
* Sign a Data Processing Agreement (DPA) with UR.
* Present the [required data-sharing declaration](#kyc-data-sharing-disclosure) to the user verbatim and capture their agreement before identity verification starts. UR does not show it for you on this path.

### Where to read more

This page focuses on the *integration* contract. For the underlying KYC process, including what UR verifies, the data UR collects, identity-verification methods, proof-of-address constraints, AML/sanctions screening, and URID status outcomes, see [KYC & compliance](/concepts/kyc-and-compliance).

For partner-engineering notes on document uploads: when a user uploads proof-of-address documents through your application, you can pre-filter for the supported languages (English, German, French, Italian) and reject unsupported documents early. Documents in other scripts can still be forwarded to UR. They will be routed to manual review by UR's support team, which adds latency.

## API signing key

UR authenticates every API call with an EIP-191 signature. There are no API keys and no OAuth tokens. Your `partnerId` is bound to a single EVM address, the public address of your signing key. You sign each request with the matching private key; UR recovers the address from the signature and compares it to the address on file for your `partnerId`. If they match, the request is authenticated.

{% hint style="warning" %}
UR never stores your private key. UR stores only your public EVM address. Keep the private key in a secure store such as a hardware security module (HSM) or a key management service (KMS). If you lose it, UR cannot recover it; you must register a new address.
{% endhint %}

You get your signing key and `partnerId` in one of two ways. Both end with the same credential: a `partnerId` plus an EVM key pair whose private key never leaves your side.

### Option 1: create it in the API sandbox *(recommended)*

The [API sandbox](https://partner.ur.app/api-sandbox) generates everything for you, so UR takes no manual action. Your `partnerId` and public address are registered the moment they are created.

{% stepper %}
{% step %}

### Sign in to the sandbox

Sign in at [partner.ur.app/api-sandbox](https://partner.ur.app/api-sandbox). On your first sign-in, the sandbox creates your `partnerId`, public address, and private key, and ties them to your account.
{% endstep %}

{% step %}

### Store your private key

Copy your private key and store it in a secure store (HSM or KMS). UR does not store it and cannot show it again later.
{% endstep %}

{% step %}

### Start signing

Use the private key to sign your API requests. Your `partnerId` and public address are already registered, so no further setup is needed from UR.
{% endstep %}
{% endstepper %}

### Option 2: bring your own EVM address

Generate the key pair yourself with any standard tool and send UR only the public address. Use this path when you want the key to originate in your own infrastructure (for example, a script, a browser wallet such as MetaMask, or a hardware wallet).

{% stepper %}
{% step %}

### Generate the key pair

Generate an ECDSA key pair (an EVM wallet) on your side. Keep the private key in a secure store (HSM or KMS).
{% endstep %}

{% step %}

### Send UR the public address

Send your public EVM address to UR through your integration channel. UR registers the address and issues your `partnerId`. You never send UR the private key.
{% endstep %}

{% step %}

### Start signing

Sign your API requests with the private key. UR verifies each signature against the address you registered.
{% endstep %}
{% endstepper %}

### After setup: sign, verify, and rotate

Once you hold a `partnerId` and key pair from either option, the runtime rules are the same:

* **Sign every request.** Sign each API request with your private key using EIP-191. UR verifies the signature on every call.
* **Verify webhooks.** For webhooks the direction reverses: UR signs the payload, and you verify it against UR's published server address.
* **Rotate when needed.** To rotate keys, register a new address and deregister the old one through your integration channel.

See [Signature and verify](https://docs.ur.app/api-reference/signature-and-verify) for the signing algorithm, the canonical payload format, and code samples.

## Co-branded card artwork

Skip this section if you do not brand the card. The card is virtual, so users add it to Apple Pay or Google Pay. You produce and submit the following through your dedicated integration channel:

* Your card artwork, exported as a flattened PNG at 1536 × 969 pixels, including the debit mark.
* The Copyright Registration for your logo.
* A signed Trademark License Agreement granting UR the right to reproduce your mark.

UR reviews your submission against Mastercard brand standards. Approval takes time, so start early. The full specification, the template download, and the submission steps live on [Co-branded debit card design](https://docs.ur.app/design/cards/co-branded-debit-card-design).

## Next steps

Once you have chosen one option from each, proceed to the detailed integration guide for your Account Mode:

* [Managed Custody Mode integration guide](/integration-methods/managed-custody-mode)
* [External Wallet Access Mode integration guide](/integration-methods/external-wallet-access-mode)

For card integration:

* **Fiat Only:** uses the standard Card endpoints in your Account Mode API reference (no separate integration surface).
* **Crypto Backed:** has a [dedicated API reference](https://docs.ur.app/api-reference/cards/crypto-backed-card) that plugs into either Account Mode.

For KYC details beyond integration mechanics:

* [KYC & compliance](/concepts/kyc-and-compliance) covers the full verification process, data collected, and status outcomes.


# What UR configures for you

The settings UR provisions for you out of band, and which ones you can read back through an API.

Some of your integration is controlled by the APIs you call. The rest is configured by UR when you onboard, through your dedicated integration channel, and is not exposed as a self-serve API. This page lists every setting in that second group in one place, says who decides it and how it is set, and tells you whether you can read it back through an API.

UR is a regulated financial product. The settings on this page carry compliance, custody, and settlement consequences, so UR provisions them per partner during onboarding rather than exposing them as runtime API calls. That keeps the boundary auditable on both sides.

The practical takeaway: record every option you confirm during onboarding and treat your own record as the source of truth. You can read a few of these settings back from an API (see [Reading your configuration back](#reading-your-configuration-back)); most you cannot, so do not rely on discovering them at runtime.

This page is a boundary reference. It does not repeat how each mode works; each row links to the page that owns the detail. For how to pick your options in the first place, see [Choose your integration options](/getting-started/integration-guide).

## The boundary at a glance

| You control at runtime via API                                                                                                                                     | UR configures out of band                                                                                                                                                                                                 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Create accounts and URIDs; create and freeze cards; set a card's default currency; pay-in, payout, FX, off-ramp; per-swipe authorization decisions (Crypto Backed) | Which Account Mode, Card Mode, and KYC Mode you run; your `partnerId` and signing key; your webhook and authorization callback URLs; Prefund Account setup and settlement rules; spending limits and regional eligibility |

## What UR configures

### Account and identity

| Setting                                                                      | Who decides                                | When and how it is set                                                                                                                                                                          | Readable via API?                  |
| ---------------------------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| **Account Mode** (Managed Custody, External Wallet Access, legacy Delegated) | You, confirmed with UR                     | Fixed at onboarding; it determines which API surface and signing model you use                                                                                                                  | No. Record it yourself             |
| `partnerId`                                                                  | UR issues it                               | Created with your key in the API sandbox, or issued when you register your own address with UR; it scopes every API call                                                                        | You hold it (issued to you)        |
| API signing key and rotation                                                 | You hold the key; UR registers the address | Create it in the API sandbox (self-serve), or generate it yourself and register the address through your integration channel; rotate by registering a new address and deregistering the old one | You hold it                        |
| Webhook URL                                                                  | You, registered by UR                      | Registered during setup; changes are coordinated with UR, with no self-serve endpoint                                                                                                           | No                                 |
| Sandbox access and test-to-live promotion                                    | UR team                                    | Requested through the UR team before production                                                                                                                                                 | Not applicable (environment state) |

See [Choose your integration options](/getting-started/integration-guide#account-mode) for the Account Mode comparison, [API signing key](/getting-started/integration-guide#api-signing-key) for the key lifecycle, and [API authentication](/getting-started/api-authentication) for the signing algorithm.

### Card

| Setting                                           | Who decides             | When and how it is set                                                                    | Readable via API?           |
| ------------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------- | --------------------------- |
| **Card Mode** (Fiat Only, Crypto Backed, No card) | You, confirmed with UR  | Fixed at onboarding and configured onto the account by UR; there is no API to enable it   | No. Record it yourself      |
| Co-branded card artwork                           | You submit; UR approves | Submitted through your integration channel; UR reviews against Mastercard brand standards | Not applicable (submission) |

See [Card Mode](/getting-started/integration-guide#card-mode) and [Co-branded card artwork](/getting-started/integration-guide#co-branded-card-artwork). The runtime card endpoints (create card, get card info, set default currency, freeze) live in your Account Mode reference, for example [Managed Custody Mode](https://docs.ur.app/api-reference/account/managed-custody-mode#id-11-card).

### Crypto Backed card

These apply only if your Card Mode is Crypto Backed.

| Setting                                           | Who decides           | When and how it is set                                                                     | Readable via API?                                                                                 |
| ------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| Prefund Account provisioning                      | UR provisions         | After you complete KYB; one account per partner                                            | Balance is readable via `GET /api/fma/v1/prefund-balance`                                         |
| Settlement currency                               | You, agreed with UR   | Agreed before onboarding; defaults to USD                                                  | Yes; the Prefund Balance response exposes a single `currency`                                     |
| Settlement mode (`fixed` or `partner_controlled`) | You, agreed with UR   | Chosen during onboarding                                                                   | No; the Prefund Balance response does not include a `settleMode` field; record it from onboarding |
| Minimum balance                                   | You, agreed with UR   | Agreed during onboarding                                                                   | Yes, as `minBalance` in the Prefund Balance response                                              |
| Off-ramp chain and deposit currency               | You, agreed with UR   | Agreed during onboarding; USDC is the only deposit currency today                          | No                                                                                                |
| Card authorization callback URL                   | You, registered by UR | Registered during onboarding; changes are coordinated with UR, with no self-serve endpoint | No                                                                                                |

See the [Crypto Backed card reference](https://docs.ur.app/api-reference/cards/crypto-backed-card), the [Prefund Balance endpoint](https://docs.ur.app/api-reference/cards/crypto-backed-card#id-3.3-get-prefund-balance), and [settlement modes](https://docs.ur.app/api-reference/cards/crypto-backed-card#id-4.1-settlement-modes).

### KYC

| Setting                                                                | Who decides            | When and how it is set                                                                                    | Readable via API?                          |
| ---------------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| **KYC Mode** (UR-hosted webview, Sumsub SDK in your app, Sumsub reuse) | You, confirmed with UR | Confirmed at onboarding                                                                                   | No. Record it yourself                     |
| Sumsub reuse setup (Donor / Recipient partners, DPA)                   | You and UR             | Configured in the Sumsub dashboard, plus a Data Processing Agreement, before the first user flows through | Not applicable (legal and dashboard setup) |

See [KYC Mode](/getting-started/integration-guide#kyc-mode) for the integration contract and [KYC & compliance](/concepts/kyc-and-compliance) for what UR verifies.

### Compliance and limits

| Setting                                                          | Who decides               | When and how it is set                                                                                                                                                                                    | Readable via API?                                                            |
| ---------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Monthly spending limit                                           | UR sets it                | Set per compliance tier when the account goes Live; the default for KYC-verified users is 30,000 CHF. A higher limit requires additional verification (Enhanced Due Diligence) and is coordinated with UR | Yes, via the on-chain `limit` query and the `monthly_limit_exceeded` webhook |
| Supported regions, restricted nationalities, account eligibility | UR maintains and enforces | Enforced during the account opening flow; you do not enforce it on your side                                                                                                                              | UR enforces at account opening; the list is published                        |

See [Supported regions](/getting-started/supported-regions) for the country and nationality lists, [Smart contracts](https://docs.ur.app/api-reference/smart-contracts) for the on-chain `limit` query, and [Webhooks](/developer-resources/webhook) for limit events.

## Reading your configuration back

Only some of the settings above can be read back through an API. Plan your integration around that split.

**Readable via API**

* **Prefund Account state:** `GET /api/fma/v1/prefund-balance` returns the account's `currency`, `balance`, `minBalance`, `level`, and `allowance` (a single settlement currency; it does **not** return `settleMode`). Treat it as the source of truth for balance and threshold rather than hardcoding them. See the [Prefund Balance endpoint](https://docs.ur.app/api-reference/cards/crypto-backed-card#id-3.3-get-prefund-balance).
* **Spending limit:** read the on-chain `limit` query for a user (returns used and total limit and the cycle start), and subscribe to the `monthly_limit_exceeded` webhook. See [Smart contracts](https://docs.ur.app/api-reference/smart-contracts) and [Webhooks](/developer-resources/webhook).

**Not exposed; record it yourself**

Your Account Mode, your per-account Card Mode, your webhook URL, your authorization callback URL, and your KYC Mode are not returned by any API. Account and card creation calls do not echo them back. Keep your own record of what you confirmed at onboarding, and treat that record as authoritative.

## Changing a setting after go-live

Every setting on this page changes through your dedicated integration channel. There are no self-serve endpoints for them, by design, so the change stays auditable. Confirm any change with UR before you rely on it in production; for URL changes (webhook or authorization callback), the switch is paired with signature validation so events keep verifying.

## Where to read more

* [Choose your integration options](/getting-started/integration-guide) for the three decisions and their setup steps.
* [Crypto Backed card reference](https://docs.ur.app/api-reference/cards/crypto-backed-card) for Prefund, settlement modes, and the authorization callback.
* [KYC & compliance](/concepts/kyc-and-compliance) for the verification process and data collected.
* [Supported regions](/getting-started/supported-regions) for eligibility gates.
* [API authentication](/getting-started/api-authentication) for signing, and [Webhooks](/developer-resources/webhook) for events.


# Sandbox environment

The sandbox is an isolated environment for testing and development, without affecting production.

## Testnet

UR provides a sandbox environment for integration development and testing.

**Base URL:** `https://urapi2-qa.ur-inc.xyz`

{% hint style="warning" %}
**KYC data on testnet is mocked.** Identity verification results are simulated and do not reflect real compliance outcomes. After completing the KYC flow on testnet, your test users will not be automatically approved; contact **<support@ur.app>** or your dedicated integration channel to have test accounts manually moved to `Live` status.
{% endhint %}


# API authentication

How authentication works when integrating with UR APIs.

UR uses [**Ethereum Personal Sign (EIP-191)**](https://eips.ethereum.org/EIPS/eip-191) for all API authentication. While there are no API keys or OAuth tokens, the concept is fundamentally the same: you register a key with UR, sign your requests with it, and UR verifies the signature on every call.

## Registering and deregistering keys

During onboarding you get an ECDSA key pair whose public address (an Ethereum address) is registered with UR. This is your authentication credential. You have two ways to obtain it: create it in the [API sandbox](https://partner.ur.app/api-sandbox), which generates the key pair and registers it for you, or generate the key pair yourself and send UR only the public address. Either way, UR stores only the public address and never your private key. You can rotate keys by registering a new address and deregistering the old one through your dedicated integration channel. For both paths and the full lifecycle, see [API signing key](/getting-started/integration-guide#api-signing-key).

## Two authentication methods

| Method                                    | Used for                                              | Who signs                          |
| ----------------------------------------- | ----------------------------------------------------- | ---------------------------------- |
| Partner authentication (server-to-server) | UR-OPEN-API and Webhooks                              | Your backend's registered key pair |
| User authentication (wallet-to-server)    | UR-API (sensitive user operations like FX, transfers) | The user's wallet                  |

## How it works

```mermaid
flowchart LR
    A[Generate key pair] --> B[Register public address with UR]
    B --> C[Sign API requests with private key]
    C --> D[UR verifies signature]
    D --> E[Request authenticated]
```

{% hint style="info" %}
For webhook verification, the flow is reversed: UR signs the response body, and your backend verifies the signature against UR's known server address.
{% endhint %}

For the full signing logic, code examples, and environment addresses, see [Signature and Verify](https://docs.ur.app/api-reference/signature-and-verify).


# Supported regions

Countries and territories where UR accounts can be opened.

As UR provides account issuance services, we handle regulatory obligations on our end. This includes complying with AML/CFT regulations, which means we can only open accounts for users in supported countries and territories.

UR accounts are issued by a regulated entity. Users in supported regions get access to Swiss IBAN accounts, SEPA Instant (EUR), SWIFT transfers, and Mastercard spending.

## Unsupported countries and territories

UR determines account eligibility by the user's country of residence. UR cannot open accounts for users who reside in the following countries and territories:

| Code | Country or territory                  | Code | Country or territory                         |
| ---- | ------------------------------------- | ---- | -------------------------------------------- |
| AFG  | Afghanistan                           | MAC  | Macao                                        |
| ALB  | Albania                               | MDG  | Madagascar                                   |
| DZA  | Algeria                               | MWI  | Malawi                                       |
| ASM  | American Samoa                        | MDV  | Maldives                                     |
| AND  | Andorra                               | MLI  | Mali                                         |
| AGO  | Angola                                | MLT  | Malta                                        |
| AIA  | Anguilla                              | MHL  | Marshall Islands                             |
| ATA  | Antarctica                            | MTQ  | Martinique                                   |
| ATG  | Antigua and Barbuda                   | MRT  | Mauritania                                   |
| ARG  | Argentina                             | MUS  | Mauritius                                    |
| ARM  | Armenia                               | MYT  | Mayotte                                      |
| ABW  | Aruba                                 | MEX  | Mexico                                       |
| AZE  | Azerbaijan                            | FSM  | Micronesia, Federated States of              |
| BHS  | Bahamas                               | MDA  | Moldova                                      |
| BHR  | Bahrain                               | MCO  | Monaco                                       |
| BGD  | Bangladesh                            | MNG  | Mongolia                                     |
| BRB  | Barbados                              | MNE  | Montenegro                                   |
| BLR  | Belarus                               | MSR  | Montserrat                                   |
| BLZ  | Belize                                | MAR  | Morocco                                      |
| BEN  | Benin                                 | MOZ  | Mozambique                                   |
| BMU  | Bermuda                               | MMR  | Myanmar                                      |
| BTN  | Bhutan                                | NAM  | Namibia                                      |
| BOL  | Bolivia                               | NRU  | Nauru                                        |
| BES  | Bonaire, Sint Eustatius and Saba      | NPL  | Nepal                                        |
| BIH  | Bosnia and Herzegovina                | NCL  | New Caledonia                                |
| BWA  | Botswana                              | NIC  | Nicaragua                                    |
| BVT  | Bouvet Island                         | NER  | Niger                                        |
| IOT  | British Indian Ocean Territory        | NGA  | Nigeria                                      |
| BRN  | Brunei Darussalam                     | NIU  | Niue                                         |
| BFA  | Burkina Faso                          | NFK  | Norfolk Island                               |
| BDI  | Burundi                               | PRK  | North Korea                                  |
| CPV  | Cabo Verde                            | MKD  | North Macedonia                              |
| KHM  | Cambodia                              | MNP  | Northern Mariana Islands                     |
| CMR  | Cameroon                              | OMN  | Oman                                         |
| CYM  | Cayman Islands                        | PAK  | Pakistan                                     |
| CAF  | Central African Republic              | PLW  | Palau                                        |
| TCD  | Chad                                  | PSE  | Palestine, State of                          |
| CHL  | Chile                                 | PAN  | Panama                                       |
| CHN  | China                                 | PNG  | Papua New Guinea                             |
| CXR  | Christmas Island                      | PRY  | Paraguay                                     |
| CCK  | Cocos (Keeling) Islands               | PER  | Peru                                         |
| COL  | Colombia                              | PHL  | Philippines                                  |
| COM  | Comoros                               | PCN  | Pitcairn                                     |
| COG  | Congo                                 | PRI  | Puerto Rico                                  |
| COD  | Congo, The Democratic Republic of the | QAT  | Qatar                                        |
| COK  | Cook Islands                          | RUS  | Russian Federation                           |
| CRI  | Costa Rica                            | RWA  | Rwanda                                       |
| CUB  | Cuba                                  | BLM  | Saint Barthélemy                             |
| CUW  | Curaçao                               | SHN  | Saint Helena, Ascension and Tristan da Cunha |
| CIV  | Côte d'Ivoire                         | KNA  | Saint Kitts and Nevis                        |
| DJI  | Djibouti                              | LCA  | Saint Lucia                                  |
| DMA  | Dominica                              | MAF  | Saint Martin (French part)                   |
| DOM  | Dominican Republic                    | SPM  | Saint Pierre and Miquelon                    |
| ECU  | Ecuador                               | VCT  | Saint Vincent and the Grenadines             |
| EGY  | Egypt                                 | WSM  | Samoa                                        |
| SLV  | El Salvador                           | SMR  | San Marino                                   |
| GNQ  | Equatorial Guinea                     | STP  | Sao Tome and Principe                        |
| ERI  | Eritrea                               | SAU  | Saudi Arabia                                 |
| SWZ  | Eswatini                              | SEN  | Senegal                                      |
| ETH  | Ethiopia                              | SRB  | Serbia                                       |
| FLK  | Falkland Islands (Malvinas)           | SYC  | Seychelles                                   |
| FJI  | Fiji                                  | SLE  | Sierra Leone                                 |
| GUF  | French Guiana                         | SXM  | Sint Maarten (Dutch part)                    |
| PYF  | French Polynesia                      | SLB  | Solomon Islands                              |
| ATF  | French Southern Territories           | SOM  | Somalia                                      |
| GAB  | Gabon                                 | ZAF  | South Africa                                 |
| GMB  | Gambia                                | SGS  | South Georgia and the South Sandwich Islands |
| GEO  | Georgia                               | SSD  | South Sudan                                  |
| GHA  | Ghana                                 | LKA  | Sri Lanka                                    |
| GIB  | Gibraltar                             | SDN  | Sudan                                        |
| GRL  | Greenland                             | SUR  | Suriname                                     |
| GRD  | Grenada                               | SYR  | Syria                                        |
| GLP  | Guadeloupe                            | TJK  | Tajikistan                                   |
| GUM  | Guam                                  | TZA  | Tanzania                                     |
| GTM  | Guatemala                             | THA  | Thailand                                     |
| GGY  | Guernsey                              | TLS  | Timor-Leste                                  |
| GIN  | Guinea                                | TGO  | Togo                                         |
| GNB  | Guinea-Bissau                         | TKL  | Tokelau                                      |
| GUY  | Guyana                                | TON  | Tonga                                        |
| HTI  | Haiti                                 | TTO  | Trinidad and Tobago                          |
| HMD  | Heard Island and McDonald Islands     | TUN  | Tunisia                                      |
| VAT  | Holy See (Vatican City State)         | TKM  | Turkmenistan                                 |
| HND  | Honduras                              | TCA  | Turks and Caicos Islands                     |
| IND  | India                                 | TUV  | Tuvalu                                       |
| IDN  | Indonesia                             | TUR  | Türkiye                                      |
| IRN  | Iran                                  | UGA  | Uganda                                       |
| IRQ  | Iraq                                  | UKR  | Ukraine                                      |
| IMN  | Isle of Man                           | USA  | United States                                |
| ISR  | Israel                                | UMI  | United States Minor Outlying Islands         |
| JAM  | Jamaica                               | URY  | Uruguay                                      |
| JEY  | Jersey                                | UZB  | Uzbekistan                                   |
| JOR  | Jordan                                | VUT  | Vanuatu                                      |
| KAZ  | Kazakhstan                            | VEN  | Venezuela                                    |
| KEN  | Kenya                                 | VNM  | Vietnam                                      |
| KIR  | Kiribati                              | VGB  | Virgin Islands, British                      |
| KWT  | Kuwait                                | VIR  | Virgin Islands, U.S.                         |
| KGZ  | Kyrgyzstan                            | WLF  | Wallis and Futuna                            |
| LAO  | Laos                                  | ESH  | Western Sahara                               |
| LBN  | Lebanon                               | YEM  | Yemen                                        |
| LSO  | Lesotho                               | ZMB  | Zambia                                       |
| LBR  | Liberia                               | ZWE  | Zimbabwe                                     |
| LBY  | Libya                                 |      |                                              |

This list is maintained in UR's support center. If this page and the [Unsupported Countries and Territories support article](https://support.ur.app/hc/en-us/articles/16480035022991-Unsupported-Countries-and-Territories) ever differ, the support article is the authoritative source.

A small set of nationalities is also excluded regardless of country of residence; see [Restricted nationalities](#restricted-nationalities) below. UR enforces both gates during the account opening flow.

## What this means for partners

As a partner, you integrate using a `partnerId` issued by UR; the wallet holding your signing key signs requests as your credential to call UR's APIs. Partner integration does not require KYB. You get the `partnerId` either from the [API sandbox](https://partner.ur.app/api-sandbox) or by registering your own EVM address with UR; see [API signing key](/getting-started/integration-guide#api-signing-key). Your users complete KYC verification, which UR runs through your app.

Users in unsupported or sanctioned jurisdictions will not be able to open a [UR Account](/concepts/ur-account). You do not need to enforce this on your side; UR handles eligibility checks during the account opening flow.

## Restricted nationalities

Due to international regulatory requirements, UR cannot currently provide services to users with certain nationalities. If your nationality is on the restricted list, you cannot complete KYC verification even if you live in a supported country. UR currently restricts the following nationalities:

| Nationality                          | Code |
| ------------------------------------ | ---- |
| American Samoa                       | ASM  |
| Guam                                 | GUM  |
| Iran                                 | IRN  |
| North Korea                          | PRK  |
| Northern Mariana Islands             | MNP  |
| Puerto Rico                          | PRI  |
| Russia                               | RUS  |
| United States Minor Outlying Islands | UMI  |
| United States of America             | USA  |

{% hint style="info" %}
Guam, American Samoa, Northern Mariana Islands, United States Minor Outlying Islands, Puerto Rico, and the United States are listed as separate ISO 3166-1 codes. They all fall under US nationality, which is currently restricted for KYC.
{% endhint %}

## Staying safe

{% hint style="warning" %}
**For your security.** UR contacts you only from official `@ur.app` email addresses or through in-app notifications. UR never asks for your password, private keys, or any sensitive credentials by email. If you receive a suspicious message claiming to be from UR, do not respond and contact <support@ur.app> immediately.
{% endhint %}


# Managed Custody Mode

API-driven integration where UR manages fiat accounts and the partner manages crypto.

In Managed Custody Mode, the partner backend is the single orchestrator for all user banking actions. UR manages a fiat account per user (tokenized deposits, IBAN, card). For compliance, the UR-managed account holds fiat only; the user's crypto is always held in an external (non-UR) wallet, either partner-side or the user's own. The user never interacts with a wallet for fiat actions.

This mode is designed for consumer fintechs, exchanges, and neobanks that want to offer banking features behind their own UX without exposing blockchain mechanics to end users.

## Architecture

In Managed Custody Mode the user's fiat account is always UR-managed and holds fiat only. The user's crypto is always held in a separate external (non-UR) wallet:

| Account               | Managed by                                                           | Holds                                                |
| --------------------- | -------------------------------------------------------------------- | ---------------------------------------------------- |
| **User Fiat Account** | UR                                                                   | Tokenized deposits (EUR, USD, CHF, etc.), IBAN, card |
| **User Crypto**       | A separate external (non-UR) wallet (partner-side or the user's own) | Crypto assets on supported chains                    |

The partner backend calls the UR API for all fiat actions (pay-in, payout, FX, on-ramp (coming soon), card). A crypto action (e.g., off-ramp) is authorized on the external wallet that holds the crypto: the user signs when it is their own wallet, or the partner signs when it is a partner-side wallet.

```mermaid
flowchart LR

subgraph User["End User"]
  U["User"]
end

subgraph Partner["Partner Platform"]
  PB["Partner Backend"]
end

subgraph UR["UR"]
  API["UR API"]
end

subgraph Accounts["User Accounts"]
  FIAT["User Fiat Account\n(UR-managed)\nTokenized deposits, IBAN"]
  CRYPTO["User Crypto\n(external non-UR wallet:\npartner-side or user's own)\nCrypto assets"]
end

subgraph Settlement["Settlement"]
  CHAIN["On-chain Settlement"]
end

U --> PB
PB -->|"API calls\n(partner signs)"| API
API --> FIAT
PB -.->|"Direct management\n(when the wallet is partner-side)"| CRYPTO
FIAT --> CHAIN
CRYPTO --> CHAIN
```

## Who signs what

| Operation             | Who authenticates                                | Notes                                                                                                                                                         |
| --------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pay-in                | Partner signs                                    | Partner fetches IBAN details via API; user sends wire from their bank                                                                                         |
| Payout                | Partner signs                                    | Partner submits payout request; UR dispatches SEPA/SWIFT                                                                                                      |
| On-ramp (coming soon) | Partner signs                                    | UR debits fiat from UR Account; crypto delivered to the user's external (non-UR) wallet                                                                       |
| Off-ramp              | Signed by the external wallet holding the crypto | Crypto is submitted to the UR off-ramp contract from the external (non-UR) wallet that holds it (partner-side or the user's own); fiat credited to UR Account |
| FX                    | Partner signs                                    | Partner submits FX swap; UR executes atomically                                                                                                               |
| Card setup            | Partner signs                                    | UR issues card; details revealed in secure UR-hosted view                                                                                                     |
| Card spend            | Automatic                                        | Settlement governed by Card Mode (Fiat Only or Crypto Backed)                                                                                                 |

## User onboarding

* The user's URID is minted into a UR-managed wallet during account creation.
* KYC has two supported paths. Pick one based on whether your platform already runs a Sumsub tenant.
  * **Sumsub reuse (share token).** Your platform completes KYC in your own Sumsub tenant and shares the approved applicant with UR. Because this path shares the user's KYC profile with UR, your KYC steps must present the required data-sharing declaration and capture the user's agreement before verification (see [the required KYC disclosure](/getting-started/integration-guide#kyc-data-sharing-disclosure)). See [API reference: Managed Custody Mode](https://docs.ur.app/api-reference/account/managed-custody-mode).
  * **Sumsub SDK in your app.** Your backend requests a UR-issued Sumsub access token and the Sumsub SDK in your app drives KYC against UR's Sumsub tenant. See [API reference: Managed Custody SDK KYC](https://docs.ur.app/api-reference/kyc-and-kyb/managed-custody-sdk-kyc).
* There is no on-chain delegation contract step (that is Delegated Mode). Your backend acts on the user's fiat account through authenticated API calls, after the user grants account access authorization (see [Account access authorization](#account-access-authorization)).
* Once KYC reaches `Live` status and the user has authorized account access, all banking functions are unlocked.

### Account access authorization

Managed Custody Mode lets your backend view a user's IBAN details and initiate transactions on their behalf through the UR API. Because of this access, you must obtain each user's explicit authorization during onboarding. The authorization has two parts: an in-app confirmation the user accepts, and a confirmation email you send afterward. Both are required for every Managed Custody Mode user.

#### In-app authorization confirmation

Show the following confirmation in your onboarding flow and record the user's acceptance before you enable banking functions. Replace `[Partner Name]` with your platform's name.

> By confirming, you expressly authorize \[Partner Name] to access your IBAN account. \[Partner Name] will be able to view your IBAN information and initiate transactions. This authorization is effective until withdrawn by you.

#### Authorization confirmation email

After the user accepts, send a confirmation email to the user's registered address. Replace `[Partner Name]` with your platform's name.

{% hint style="warning" %}
BCC `legal@ur.app` on every authorization confirmation email. This BCC is mandatory; it gives UR a compliance record that the user authorized account access.
{% endhint %}

```
Subject: Your [Partner Name] IBAN Account Access Has Been Authorized

Dear Customer,

Your IBAN account with [Partner Name], powered by UR, has been successfully created.

By completing the registration and using the related services, you authorize [Partner Name] to:
- view your IBAN information
- initiate transactions

You may revoke this authorization at any time by contacting [Partner Name]'s Customer Service. If you did not initiate this account opening or do not consent to this arrangement, please immediately notify support@ur.app.

Best regards,
The [Partner Name] Team
```

## Core banking functions

### Pay-in

The user sends a bank transfer (SEPA or SWIFT) from their external bank to their UR IBAN. The partner fetches the user's deposit details via the Profile API, displays them, and UR credits the fiat balance when the wire settles. UR notifies the partner via webhook.

### Payout

The partner submits a payout request on behalf of the user. UR debits the fiat balance and dispatches the bank transfer. The user does not sign; the partner backend authorizes the action.

### Off-ramp (crypto to fiat)

The user initiates an off-ramp from the partner UI. The partner fetches a quote from UR, then the crypto is submitted to the UR off-ramp contract from the external (non-UR) wallet that holds it (partner-side or the user's own). Fiat is credited to the user's UR Account. The off-ramp contract is agnostic to the source wallet; the `_targetAccount` parameter names the UR Account that receives the fiat (see [API reference §7.2](https://docs.ur.app/api-reference/account/managed-custody-mode#id-7.2-initiate-off-ramp)). UR notifies the partner via webhook.

### On-ramp (fiat to crypto)

{% hint style="warning" %}
**Available soon.** On-ramp (fiat-to-crypto) is not yet available for integration and will be enabled in a future release. The reference below is provided for preview only.
{% endhint %}

The partner fetches a quote and the user confirms. The partner submits the on-ramp request; UR debits the user's fiat balance and delivers the crypto to the user's external (non-UR) wallet. The user does not sign for the fiat debit.

### FX (fiat to fiat)

The partner fetches a quote, the user confirms, and the partner submits the swap. UR executes the conversion atomically (burn one tokenized deposit, mint another). The user does not sign.

### Card

The partner requests card issuance via API. UR issues the card; the card number and CVV are revealed only inside a secure UR-hosted view. Card spend behavior is governed by Card Mode (Fiat Only or Crypto Backed), which is chosen independently of the integration mode.

## API reference

See [API reference: Managed Custody Mode](https://docs.ur.app/api-reference/account/managed-custody-mode) for endpoints, request/response formats, and webhook definitions.


# External Wallet Access Mode

Self-custody integration where users connect their own wallet and sign transactions directly.

In External Wallet Access Mode, users connect their own Web3 wallet (MetaMask, Rabby, SafePal, etc.) and maintain full self-custody of their keys. The URID is minted directly to their wallet address.

This mode is designed for partners building Web3-native products where users expect to sign transactions themselves.

## User onboarding

User onboarding is designed for Web3 wallet users to apply for a UR Account. The process focuses on cryptographically verifying wallet ownership and minting the URID (Identity NFT) directly to the user's wallet, ensuring they maintain full control of their identity from day one.

```mermaid
sequenceDiagram

    autonumber

    actor User

    participant App as Partner App

    participant UR as UR Backend

    participant Contract as UR Contract (Mantle)

    participant RegEntity as UR Regulated Entity

    rect rgb(240, 248, 255)

    note right of User: Phase 1: Identity Initialization (Tourist)

    User->>App: Request Fiat Account (IBAN)

    App->>User: Require email for registration

    User->>App: Input email

    App->>UR: POST /kyc/emailStatus (validate email)

    note right of App: Invalid if email already exists on system<br/>User must provide different email

    alt Email Valid & Available

        App->>UR: POST /mint (walletAddress, email, partnerId)

        note right of UR: UR validates & prepares mint transaction

        UR->>Contract: Mint URID NFT (Status: Tourist)

        Contract-->>UR: Tx Hash & Confirmation

        UR-->>App: Return URID, Status (Tourist), Tx Hash

        note right of App: Store URID locally for session



    else Email Invalid or Taken

        UR-->>App: Error: Email unavailable

        App->>User: Display error & request new email

    end

    end


    rect rgb(255, 250, 240)

    note right of User: Phase 2: KYC Data Collection

    App->>UR: POST /kyc/token (URID)

    UR-->>App: Return Sumsub Token

    note right of UR: Token scoped to user URID

    App->>User: Launch Sumsub SDK (Face + ID Verification)

    User-->>App: Complete KYC verification flow

    note right of User: User submits documents directly to Sumsub

    UR->>App: Webhook: /kyc/completion (URID, status)

    note right of UR: Triggered when Sumsub data collection completes<br/>Does NOT mean approval: just data received

    end

    rect rgb(240, 255, 240)

    note right of User: Phase 3: Document Signature & On-Chain Submission

    UR->>App: Webhook: /kyc/signatureRequest

    note right of UR: Payload contains:<br/>- Full formatted legal text (documentText)<br/>- Structured user data (documentData)<br/>- Document hash to sign<br/>- EIP-712 typed data structure

    App->>User: Display Form A with formatted text + signature prompt
    note right of User: User reviews legal declaration<br/>Partner must display documentText verbatim<br/>User signs with their wallet (EIP-712)
    User-->>App: Return wallet signature (v, r, s)

    App->>UR: POST /kyc/submit (URID, signature, documentHash)
    note right of UR: UR validates:<br/>- Signature matches documentHash<br/>- Signer is URID owner<br/>- Hash hasn't been used before

    UR->>RegEntity: Submit KYC package for compliance review

    note right of RegEntity: Asynchronous approval process<br/>Duration: 1-5 business days typical
    UR-->>App: Return 202 Accepted (pending review)

    App->>User: Display "Under Review" status

    end

    rect rgb(230, 230, 250)
    note right of User: Phase 4: Approval & Finalization

    alt Regulated entity approves account
        RegEntity-->>UR: Webhook: Account Approved + IBAN details
        UR->>Contract: Update URID Status → Live

        Contract-->>UR: Tx Hash & Confirmation
        UR->>App: Webhook: /kyc/result (status: LIVE, URID)

        App->>UR: GET /kyc/document (URID)

        note right of UR: Returns finalized, stamped PDF
        UR-->>App: Return final signed KYC PDF
        App->>UR: GET /profile (URID, signature)

        UR-->>App: Return account details (IBAN, account number, address)

        App->>User: Display account active + IBAN details

    else Regulated entity rejects account

        RegEntity-->>UR: Webhook: Account Rejected + reason code

        UR->>Contract: Update URID Status → Rejected

        Contract-->>UR: Tx Hash & Confirmation

        UR->>App: Webhook: /kyc/result (status: REJECTED, reason)

        App->>User: Display rejection reason + next steps

    end

    end
```

**Flow Description:**

* Identity Initialization: The Partner App first[ validates the user's email availability](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-2.1.2-email-verification) and, upon success, triggers the [minting](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-2.1.4-mint-create-tokenid) of a "Tourist" status URID NFT associated with the user's wallet address.
* KYC Data Collection: The Partner integrates the Sumsub SDK. The partner's frontend [requests a specific Sumsub token](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-2.1.5-get-sumsub-sdk-token) scoped to the user's URID and launches the SDK to collect identity documents and biometric data. A [webhook notifies the Partner](/developer-resources/webhook#event-sumsub_kyc_result) when data collection is complete.

{% hint style="warning" %}
**NFC scanning requires a mobile app.** The Sumsub SDK's Passport/National ID NFC scan step is only supported in the **Sumsub mobile SDK** (iOS/Android). It is not available in the Sumsub web SDK. Partners integrating via the External Wallet Access Mode must surface this KYC step through their **mobile app**. Users on a web browser cannot complete NFC-based identity verification. If your platform is web-only, email <support@ur.app> or use your dedicated integration channel to discuss alternatives.
{% endhint %}

* Document Signature: UR generates 'Form A' (a legal declaration required by regulation) for users to sign. Upon receiving the webhook notification confirming the completion of KYC data collection, the Partner must[ fetch the user's KYC document](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-2.1.8-get-user-kyc-document-form-a) and display the information to the user. The user cryptographically signs this document using their external wallet (EIP-712 standard), and the Partner [submits this signature to UR](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-2.1.9-submit-user-signature) for compliance validation.
* Approval & Finalization: Once the asynchronous compliance review is completed, UR updates the on-chain status to "Live" and[ notifies the Partner](/developer-resources/webhook#event-kyc_status). The Partner can then [retrieve the user's profile details ](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-3.1.1-get-user-profile)(IBAN, etc.).

## Card

Card Operations in External Wallet Access Mode are designed for users to manage their co-branded debit card directly from their self-custody wallet. High risk actions, such as setting spending limits, card activation, and viewing card details, are secured through user's offline signatures.

```mermaid

sequenceDiagram
    autonumber
    actor User
    participant App as Partner App
    participant UR as UR Backend
    participant Contract as UR Contract
    participant Webview as Secure Webview

    rect rgb(240, 248, 255)
    note right of User: Phase 1: Check Card Status
    User->>App: Access Card Section
    App->>UR: API: GET /card
    UR-->>App: Return Card Info OR Empty/404
    end

    alt User has NO Card (Activation Flow)
        rect rgb(255, 250, 240)
        note right of User: Phase 2: Activation & Permit

        User->>App: Request for a card
        App->>User: Request a signature
        App->>UR: API: POST /card  (API request includes signature)
        UR-->>App: Return Basic Card Info
        App->>User: Prompt: Select Currency to Enable
        User-->>App: Select Currency (e.g. USD)
        App->>User: Request Offline Signature (Permit)
        note right of User: User signs EIP-2612 Message<br/>(No Gas Fee)
        User-->>App: Return Signature
        App->>UR: API: POST /token-permit (with Signature)
        note right of UR: UR pays Gas to submit<br/>authorization to chain
        UR->>Contract: Execute Permit & Approve
        Contract-->>UR: Success
        UR-->>App: Return: Authorization Enabled
        end
    else User HAS Card
        note right of App: Skip Activation
    end

    rect rgb(240, 255, 240)
    note right of User: Phase 3: Display & Sensitive Data
    App->>User: Show Basic Info<br/>(Masked PAN, Limit, Design)
    User->>App: Click "Show Card Details"
    App->>Webview: Load UR card display script
    Note over Webview: PCI DSS Scope (Secure Zone)
    Webview->>User: Render Full Card Details

    end
```

* Status Check: The Partner App [queries the current card status](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-3.1.1-get-user-profile) to determine if the user needs to activate a new card or view an existing one.
* [Activation](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-3.1.2-create-card) (If applicable): If no card exists, the user initiates an activation request. This involves signing a request to create the card.
* [Set Allowance](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-3.1.8-permit-token-approval): The user grants spending authority over their UR account balance (on-chain Fiat tokens) by generating an EIP-2612 "Permit" signature. This offline signature authorizes the default currency allowance, with UR handling the subsequent on-chain submission and gas payment.
* Secure Display: To view sensitive data (PAN, CVV), the user first calls the [card information API](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-3.1.3-get-card-info). The response includes a `cardToken` alongside basic card details. The Partner Frontend loads UR's card display script and passes the token into [`window.fiat24card.bootstrap(...)`](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-3.1.4-get-card-details) to display the sensitive data within a secure zone.

## Off-ramp flow (crypto-to-fiat)

This flow enables users to convert held crypto assets into fiat balances within their UR account. UR supports two chain families:

* **EVM chains** (Ethereum, Arbitrum, Mantle, etc.): the user interacts directly with [UR Contracts](https://docs.ur.app/api-reference/smart-contracts#contract-addresses) via their EVM wallet. See [§4.1 Offramp API for EVM](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-4.1.1-get-quote).
* **Solana**: the user deposits USDC via a Solana wallet. The Partner calls [§4.2 Offramp API for Solana](https://docs.ur.app/api-reference/account/external-wallet-access-mode#4.2-offramp-api-for-solana) to get a pre-built transaction, which the user signs and submits on Solana. The USDC is bridged cross-chain to Mantle via LayerZero and converted to fiat.

The diagrams below illustrate the **EVM** path. For the Solana-specific flow (quote → sign → send → cross-chain settlement), refer to [§4.2](https://docs.ur.app/api-reference/account/external-wallet-access-mode#4.2-offramp-api-for-solana) in the API Reference.

```mermaid

sequenceDiagram
    autonumber
    actor User
    participant App as Partner App
    participant UR as UR Backend
    participant Contract as UR Contract

    rect rgb(240, 248, 255)
    note right of User: Phase 1: Quotation
    User->>App: Input USDC Amount (e.g. 100 USDC)
    App->>UR: POST /quote/deposit (Amount, Chain)
    UR-->>App: Return Quote<br/>(Exchange Rate, Fees, Est. Fiat Output, Error code)
    App-->>User: Display Estimated Fiat Received
    end

    rect rgb(255, 250, 240)
    note right of User: Phase 2: Permit & Execution
    User->>App: Confirm Transaction
    App->>User: Request Wallet Signature (requires gas fee)
    User->>Contract: Sign the offramp transaction

    Contract-->>App: Return txHash

    App-->>User: Show "Status: Pending / Processing"
    end
    rect rgb(240, 255, 240)

    note right of User: Phase 3: Settlement

App->>Contract: Start Monitoring (Listening for Event)

    alt Event Received (Within 5 mins)
        Contract-->>App: Event: DepositSuccess
        par Update UI
            App-->>User: Show "Deposit Successful"
        end
    else Timeout (No Event > 5 mins)

        Note right of App: Monitoring exceeded time limit
            App->>UR: API: Report Timeout
            UR->>Ops: 🚨 Alert: Deposit Stuck (TxHash)
            Ops->>UR: Fix status
            UR->>App: Fix status
    end
    end
```

* Quotation: The user requests a conversion amount (e.g., USDC), and the Partner frontend[ fetches a quote](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-4.1.1-get-quote) containing the exchange rate, fees, and estimated fiat output.
* Execution: Upon confirmation, the Partner App prompts the user to sign and submit the transaction directly to the UR Contract.
  * > Note: In this mode, the user pays the gas fee for the on-chain transaction.
* Settlement: The Partner App monitors the blockchain for the specific `DepositSuccess` event. If confirmed within the timeout window, the UI is updated; otherwise, the Partner reports the timeout to UR operations for reconciliation.

## On-ramp flow (fiat-to-crypto)

{% hint style="warning" %}
**Available soon.** On-ramp (fiat-to-crypto) is not yet available for integration and will be enabled in a future release. The reference below is provided for preview only.
{% endhint %}

The On-ramp process in External Wallet Access Mode is designed to convert UR fiat balances into crypto assets using a gasless execution model. Instead of broadcasting the transaction themselves, the user provides a [EIP-2612 signature (Permit) ](https://eips.ethereum.org/EIPS/eip-2612)authorizing the trade, which the Partner submits to onchain via API to help users to abstract gas fees from the transaction.

```mermaid

sequenceDiagram
    autonumber
    actor User
    participant App as Partner App
    participant Webview as Liveness Webview
    participant UR as UR Backend
    participant Contract as UR Contract

    rect rgb(240, 248, 255)
    note right of User: Phase 1: Quotation & Risk Check
    User->>App: Input Onramp Request<br/>(e.g., 1000 EUR to ETH)
    App->>UR: API: GET /quote/onramp (User, Amount)

    UR->>UR: Check Risk Rules
    note right of UR: 1. Monthly Quota(will failed if a TX used the limit up)<br/>2. Min/Max Limit(Unacceptable input)<br/>3. Liveness Status(user have to pass a check if this is true)

    UR-->>App: Return Quote + Risk Flags<br/>{rate, fee, needsLiveness: T/F}
    end

    alt If Liveness Check Required (Risk Triggered)
        rect rgb(255, 230, 230)
        note right of User: Phase 1.5: Step-up Verification
        App->>User: Prompt "Verification Needed"
        User->>App: Confirm
        App->>Webview: Initialize Liveness URL
        User->>Webview: Perform Face Scan
        Webview->>UR: Verify Biometrics
        UR->>UR: Update User Risk Profile
        UR-->>App: Webhook/Callback: Verification Success

        App->>UR: GET /quote/onramp (Re-fetch)
        note right of App: Refresh Quote with<br/>new permissions
        UR-->>App: Return Final Quote (Allowed)
        end
    else Liveness Not Required
        note right of App: Proceed with existing Quote
    end

    rect rgb(255, 250, 240)
    note right of User: Phase 2: Execution (Gasless)
    User->>App: Accept Quote & Confirm
    App->>User: Request Offline Signature (Permit)
    User-->>App: Return Signature

    App->>UR: API: POST /onramp (Signature, QuoteID)
    UR->>UR: Validate Sig & Quote Expiry
    UR->>Contract: Execute permitOnramp()
    note right of UR: UR pays Gas Fee
    UR-->>App: Return txHash
    end

    rect rgb(240, 255, 240)
    note right of User: Phase 3: Settlement
    App->>Contract: Monitor txHash
alt Transaction Confirmed (Within 5 mins)
        Contract-->>App: Event: Success
        par Notify User
            App-->>User: Show "Onramp Successful"
        end

    else Timeout (No Confirmation > 5 mins)
        Note right of App: Monitoring exceeded limit

        par User Feedback
            App-->>User: Show "Status: Pending / Processing"
            Note right of User: "Transaction submitted.<br/>Waiting for network confirmation."
        and Escalation
            App->>UR: Report Timeout Status
            UR->>Ops: 🚨 Alert: Onramp Stuck (TxHash)
            Note right of Ops: Re-trigger
            Ops->>UR: Fixing status
            UR->>App: Return the estimated processing time.
        end
    end
    end

```

* Quotation & Risk: The Partner App requests an [onramp quote](https://docs.ur.app/api-reference/account/external-wallet-access-mode#onramp). UR's backend performs real-time risk checks and returns the quote along with any required verification flags.
* Step-up Verification (If required): If the transaction triggers a risk rule (e.g., Liveness Check), the Partner App must redirect the user to a verification [Webview](https://docs.ur.app/api-reference/account/external-wallet-access-mode#onramp) before the user can continue the onramp request.
* Gasless Execution: The user accepts the quote and provides an offline signature (Permit). The Partner frontend [submits this signature via API](https://docs.ur.app/api-reference/account/external-wallet-access-mode#onramp). UR validates the signature and executes the onramp contract on-chain, covering the gas fees on behalf of the user.
* Settlement: The Partner App monitors the transaction hash for confirmation and updates the user interface upon success or reports a timeout if the network is congested.

## Fiat-to-fiat (FX)

The FX flow allows users to exchange one fiat token for another by signing an off-chain authorization and letting UR execute the transaction on-chain.

```mermaid
sequenceDiagram
    autonumber
    actor User
    participant App as Partner App
    participant UR as UR Backend
    participant Contract as UR Contract

    rect rgb(240, 248, 255)
    note right of User: Phase 1: Quotation
    User->>App: Input Fiat Amount (e.g., 100 EUR24 to USD24)
    App->>UR: POST /fx/quote
    UR-->>App: Return Quote (rate, fee, estimated output, errors)
    App-->>User: Display Estimated Output
    end

    rect rgb(255, 250, 240)
    note right of User: Phase 2: Permit & Execution
    User->>App: Confirm Transaction
    App->>User: Request Wallet Signature
    User-->>App: Sign permit payload
    App->>UR: POST /fx-permit
    UR->>Contract: Execute FX using user permit
    Contract-->>UR: Return txHash
    UR-->>App: Return txHash
    App-->>User: Show "Status: Pending / Processing"
    end

    rect rgb(240, 255, 240)
    note right of User: Phase 3: Status Update
    alt FX Executed
        UR-->>App: Webhook /webhook/fx.approved
        App-->>User: Show "Status: Completed"
    else FX Rejected
        UR-->>App: Webhook /webhook/fx.rejected
        App-->>User: Show "Status: Rejected"
    end
    end
```

**Flow Description:**

* Quotation: The Partner frontend calls `POST /fx/quote` with input token, output token, and amount, then shows the user the estimated output, fees, and effective rate.
* Permit & Execution: After user confirmation, the app collects a wallet signature and submits `POST /fx-permit`. UR verifies the signature and executes the exchange on-chain.
* Status Update: The Partner receives asynchronous webhook updates (`fx.approved` or `fx.rejected`) and updates UI and balances.
* Fee Handling: Per PRD, network costs are deducted from the user's input fiat amount.

## Cash pay-in

Users' bank account details can be retrieved via the Profile API. EUR and CHF transfers must come from a same-name account and use the user's default Swiss IBAN, which UR issues automatically when the user reaches `Live`. USD uses a separate USD IBAN, which UR issues only on request: call `POST /v1/apply-usd-payin` when the user wants to receive USD. The call is synchronous, and UR issues the USD IBAN immediately if the user is `Live`, with no manual review. For how UR handles USD deposits, including transfers from a third-party account, see [Deposits](/money-movement/deposits#usd-deposits-from-a-third-party-account).

## Cash pay-out (bank transfer)

This flow covers direct bank transfer payouts with recipient verification, signed authorization, and asynchronous transfer/refund webhooks.

```mermaid
sequenceDiagram
    autonumber
    actor User
    participant App as Partner App
    participant UR as UR Backend
    participant Contract as UR Contract

    rect rgb(240, 248, 255)
    note right of User: Phase 1: Create/Select Recipient
    User->>App: Select transfer currency
    App->>UR: GET /banks/payout/fees
    UR-->>App: Return payout fee metadata by currency
    App->>UR: GET /profile (contact list)
    App->>UR: GET /payment-purposes

    alt Existing recipient
        User->>App: Select contact + purpose + reference
        App->>UR: POST /verify-reference
        UR-->>App: Return refId
    else New recipient
        User->>App: Create new transfer
        App->>UR: GET /banks
        User->>App: Select recipient bank country
        alt Country supports IBAN metadata
            User->>App: Input IBAN
            App->>UR: GET /banks/iban/{ibanNo}
            UR-->>App: Return bank details
        else Country requires full bank details
            User->>App: Select bank and input bank account info
        end
        App->>UR: GET /country-cities
        User->>App: Input recipient details + purpose + reference
        App->>UR: POST /verify-contact (bankPaymentRequest)
        UR-->>App: Return contactId, purposeId, refId
    end
    end

    rect rgb(255, 250, 240)
    note right of User: Phase 2: Permit & Transfer
    User->>App: Input amount and confirm transfer
    App->>User: Request wallet signature
    User-->>App: Sign permit (refId, contactId, purposeId)
    App->>UR: POST /payout-with-permit
    UR->>Contract: Execute client payout
    Contract-->>UR: Return txHash
    UR-->>App: Return txHash
    App-->>User: Show "Status: Pending / Processing"
    end

    rect rgb(240, 255, 240)
    note right of User: Phase 3: Review & Status
    alt Transfer approved
        UR-->>App: Webhook /webhook/transfer.approved
        App-->>User: Show "Funds sent"
    else Transfer rejected then refunded
        UR-->>App: Webhook /webhook/transfer.rejected
        App-->>User: Show "Transfer failed, refunding..."
        UR-->>App: Webhook /webhook/transfer.refunded
        App-->>User: Show "Refund completed"
    end
    end
```

**Flow Description:**

* Recipient Setup: The user either [selects an existing contact or creates a new recipient](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-6.1-create-select-recipient).
  * Create a contact:
    * Get the supported bank\&country list from the [bank country API](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-6.1.1-get-supported-banks). User can select the country from the returned list.
      * If the selected country has `ibanMetadata` , you can let users to input IBAN, and use [this API](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-6.1.2-get-bank-by-iban) to verify and retrieve bank info .
      * If the selected country is a non-IBAN country (no `ibanMetadata` value), you will need users to select the bank name from the list and input the bank account.
    * Recipient's personal info should be input carefully, includes input name, select [country and city](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-6.1.3-get-supported-recipient-countries-and-cities), select [payout purpose](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-6.1.4-get-payment-purpose-list), inout address and and reference.
      * All the info should be [verified by this API](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-6.1.6-verify-contact-bank-payment-request), if the recipient info is valid, a `contactId`, a `refId` and a `purposeId` will be returned. These three parameters are mandatory prerequisites for proceeding to the payout.
  * Select a contact:
    * Recipient Selection: Users can select a pre-saved recipient retrieved via the [profile API](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-3.1.1-get-user-profile). The `contactId` and are automatically derived from the selected recipient profile.
      * Reference Validation: The user manually inputs the payment reference, which is then verified [via the API](https://docs.ur.app/api-reference/account/external-wallet-access-mode) to generate the `refId` and `purposeId` .
      * With the 3 parameters collected, user can proceed the payout.
* Permit and Payout : The app loads [payout fee metadata](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-6.2.1-get-fees) early and uses the selected currency's `fee` and `minimalPayoutAmount` when presenting the transfer form. It then [collects a wallet signature](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-6.2.2-create-payout-permit-request) tied to `refId`, `contactId`, and `purposeId` before calling the [payout API](https://docs.ur.app/api-reference/account/external-wallet-access-mode#id-6.2.2-create-payout-permit-request).
* On-chain Execution: UR submits the payout transaction and returns `txHash` for partner-side status tracking.
* Asynchronous Final State: Final outcome is delivered by webhooks (`transfer.approved`, `transfer.rejected`, `transfer.refunded`). If rejected, refund completion is notified separately.
* Input Constraints: Name/address/reference fields should use Latin characters only. Gas fee is deducted from the user's fiat transfer amount.


# Delegated Mode

API-driven integration where UR manages blockchain complexity on your behalf.

{% hint style="warning" %}
**Will be deprecated soon.** Delegated Mode is no longer available for new integrations. Existing partners on this mode continue to be supported. New partners should use [Managed Custody Mode](/integration-methods/managed-custody-mode), which provides the same API-driven experience without the delegation ceremony.
{% endhint %}

In Delegated Mode, your platform interacts with UR through REST APIs. Users authenticate once via the UR Webview to complete KYC and grant delegation permissions. After that, your platform can operate their UR Account via API without requiring further user wallet signatures.

This mode is designed for partners who want a Web2-style integration with minimal blockchain exposure for their users.

## User onboarding

You redirect the user to a secure UR Webview. Within this Webview, the user completes identity verification (KYC), creates their UR Account and URID, and signs an approval to delegate access to your platform. Once complete, UR redirects the user back to your app with an authorization code.

Unlike External Wallet Access Mode, the user does not sign every action with a personal wallet; they perform a one-time delegation setup here.

**Authorization Flow Diagram**

```mermaid

sequenceDiagram
    participant User as User Browser
    participant Partner as Partner Server
    participant UR as UR Authorization Server
    participant MiniDapp as UR Mini Dapp

    Partner->>Partner: 1. Construct authorization URL
    Partner->>User: 2. Display authorization link
    User->>MiniDapp: 3. Click to visit authorization page
    Note over MiniDapp: 4. Parse URL parameters<br/>(client_id, redirect_uri, scope, state)
    User->>MiniDapp: 5. Complete KYC and authorization
    Note over UR: 6. Generate authorization code
    UR->>User: 7. Redirect to redirect_uri
    Note over User: redirect_uri?code=xxx&state=xxx
    User->>Partner: 8. Browser redirects to Partner
    Partner->>UR: 9. Call API to exchange code for user info<br/>(with signature verification)
    UR->>Partner: 10. Return user info and authorization details
    Partner->>User: 11. Complete business flow
```

**Flow Description:**

* **Initialization**: The user initiates the process on the Partner App. The Partner [constructs a standard OAuth-style Authorization URL](https://docs.ur.app/api-reference/account/delegated-contract-mode#authorization-url-construction) and redirects the user to UR.
* **Onboarding** (UR Side): The user lands on the secure UR Webview and authenticates via social login (Google or Email OTP, managed by the UR account system). A UR account is automatically created along with a unique URID. The user completes the KYC process (Liveness/ID scan) directly within the UR Webview. The initial status of the URID is set to "Tourist".
* **Delegation (Critical Step):** Upon submitting KYC data for review, UR prompts the user to grant permissions to the Delegated Contract that is dedicated to the Partner. This authorization enables the Partner to execute future actions (such as offramps) via API without requiring blockchain interaction for every transaction.
* **Account creation**: UR redirects the user back to the Partner's `redirect_uri` with an Authorization Code. The Partner App [to retrieve the user's `URID`, `ETH Address`, and status](https://docs.ur.app/api-reference/account/delegated-contract-mode#partner-to-fetch-user-onboarding-information), completing the binding process.
* **Finalization**: Once KYC has been approved, the URID status will become live. Follow up functions will be then unlocked for the user.
* **KYC retry**: This step is an **exception handling mechanism** and is not part of the standard workflow. Following the initial KYC submission, UR may occasionally flag specific verification steps for retry (e.g., due to document quality issues). This status is updated dynamically via the [profile](https://docs.ur.app/api-reference/account/delegated-contract-mode#fetch-ur-account-information) API. <mark style="color:$primary;">Best Practice: We recommend performing a status check on this field before the user initiates any UR-related features. The user should be redirected to</mark> [<mark style="color:$primary;">get.ur.app</mark>](https://get.ur.app) <mark style="color:$primary;">to complete the retry process if and only if the</mark> <mark style="color:$primary;">`kycRetryVerificationLevel`</mark> <mark style="color:$primary;">field returns a non-zero value.</mark><br>

## Card

Once the user completes onboarding and their URID status becomes **'Live'**, they are eligible for card issuance. Critical operations, such as card activation, revealing card details (PAN/CVV), and managing card settings, are performed by redirecting the user to the **UR Card Webview**. [`Refer to this document`](https://docs.ur.app/api-reference/account/delegated-contract-mode#card) for details of integrating the Card webview integration.

## Profile

Some users may need to view their IBAN information, so we provide an integration for the **Profile Webview**. Partners can integrate this webview into their app as either a webview or a dialog. For detailed integration instructions, please refer to [`this document`](https://docs.ur.app/api-reference/account/delegated-contract-mode#profile).

## Bank transfer

For users who need to move funds between their UR account and external bank accounts, we provide an integration for the **Bank Transfer Webview**. Partners can integrate this webview into their app as either a webview or a dialog. For detailed integration instructions, please refer to [`this document`](https://docs.ur.app/api-reference/account/delegated-contract-mode#bank-transfer).

## Core banking functions

### Crypto-to-fiat (off-ramp)

```mermaid
sequenceDiagram
    autonumber
    actor User
    participant App as Partner App
    participant UR as UR Backend
    participant Delegator as UR Delegator Contract
    participant Chain as UR Chain

    rect rgb(240, 248, 255)
    note right of User: Phase 1: Quotation
    User->>App: Request USDC offramp in my UR account (e.g. 100 USDC)
    App->>UR: POST /quote/deposit (Amount, Chain)
    UR-->>App: Return Quote<br/>(Exchange Rate, Fees, Est. Fiat Output, Error code)
    App-->>User: Display Estimated Fiat Received
    end

    rect rgb(255, 250, 240)
    note right of User: Phase 2: Permit & Execution
    User->>App: Confirm Transaction
    App->> Chain: Transfer USDC to User's UR account
    Chain->> App: Event: Transfer confirmed 

    App->>UR: API: Create Offramp Request

    UR->>Delegator: request offramp for the user's UR account 
    UR->>App: API response: TX hash
    App-->>User: Show "Status: Processing"
    end

    rect rgb(240, 255, 240)
    note right of User: Phase 3: Settlement
    UR->>Delegator: Start Monitoring (Listening for Event)
    alt Event Offramp Done (Within 5 mins)
        Delegator->>Chain: Done the offramp and send fiat to user's UR account
        Delegator-->>UR: Event: DepositSuccess
        UR->>App: Webhook: Offramp final status
        App-->>User: Show "Status: Success"

    else Timeout (No Webhook report > 5 mins)
        Note right of App: Monitoring exceeded time limit
        
            App->>UR: API: Report Timeout
            UR->>Ops: 🚨 Alert: Deposit Stuck (TxHash)
            Ops->>UR: Fix status
            UR->>App: Fix status
    end
    end
```

**Core Mechanism**

* **Quotation & Pricing**: The Partner requests a real-time exchange rate and fee structure via the [Offramp quote API](https://docs.ur.app/api-reference/account/delegated-contract-mode#fetch-offramp-quote). This ensures the user sees the exact estimated fiat output before confirming.
* **API-Driven Execution**: Once the user confirms the trade on the Partner's UI, the Partner triggers the transaction by calling the [Offramp API](https://docs.ur.app/api-reference/account/delegated-contract-mode#create-offramp-request). Critically, this step does not require a user wallet signature. The [Partner signs the API request](https://docs.ur.app/api-reference/signature-and-verify) using their [own key](https://docs.ur.app/api-reference/signature-and-verify#id-1.-generate-key-pair).
* **On-Chain Settlement**: UR's backend validates the request and triggers the Delegated Contract on the Mantle Network. The contract verifies the user's pre-authorized allowance, transfers the authorized crypto assets (e.g., USDC), and credits the user's UR account with the corresponding fiat balance (e.g., USD).
* **Asynchronous Notification**: Upon successful on-chain settlement, UR notifies the Partner via a [webhook](/developer-resources/webhook#event-transaction), allowing the Partner to update the user's UI to reflect the completed transaction.

### Fiat-to-crypto (on-ramp)

{% hint style="warning" %}
**Available soon.** On-ramp (fiat-to-crypto) is not yet available for integration and will be enabled in a future release. See [External Wallet Access Mode](/integration-methods/external-wallet-access-mode#on-ramp-flow-fiat-to-crypto) for the on-ramp implementation reference.
{% endhint %}

### Fiat-to-fiat (FX)

{% hint style="info" %}
Coming soon. See the [External Wallet Access Mode](/integration-methods/external-wallet-access-mode#fiat-to-fiat-fx) for the current FX implementation.
{% endhint %}

### Cash pay-in

{% hint style="info" %}
Coming soon. Users' bank account details can be retrieved via the Profile API. Currently, bank transfers are limited to same-name accounts in EUR and CHF only.
{% endhint %}

### Cash pay-out (bank transfer)

{% hint style="info" %}
Coming soon. See the [External Wallet Access Mode](/integration-methods/external-wallet-access-mode#cash-pay-out-bank-transfer) for the current bank transfer implementation.
{% endhint %}


# Core banking overview

An overview of money movement on UR.

This guide provides a high-level overview of the core products that power all money movement on the UR platform. These modules are the engine that connects your platform to the global banking system and creates a seamless, closed-loop financial ecosystem for your users.

You can use these products individually or combine them to create sophisticated applications, from simple "cash-out" features to a fully embedded banking experience.

#### Key capabilities

The core banking suite is built on two primary pillars:

* **Fiat money movement:** Moving fiat (e.g., EUR, CHF) between a user's UR account and the traditional banking system.
* **Asset conversions:** Instantly converting assets between crypto (held on your platform) and fiat (held in the user's UR account).

### Fiat money movement (bank transfers)

This pillar connects your user's account to the traditional banking world, allowing them to move funds to and from any external bank account. These features are generally reserved for fully verified, `Live` status users.

#### Fiat deposits (external)

* **What it is:** This flow allows a user to fund their UR account by sending a bank transfer *from* an external source (e.g., their salary, another bank account).
* **How it works:** After a user is fully verified (`Live`), you can call our API to retrieve their personal Swiss IBAN. The user then uses this IBAN to send a SEPA or SWIFT transfer. When the funds arrive, we send you a `payment.received` webhook so you can update their balance in your app.
* **Use case:** Enabling users to "cash in" to the ecosystem or receive payments from third parties.

#### Fiat payouts (external)

* **What it is:** This flow allows a user to withdraw fiat from their UR account and send it *to* an external bank account.
* **How it works:** This is a high-security action. Your platform initiates the request, and we provide a secure webview for the user to authenticate (via Email/SSO) and authorize the on-chain transfer. This provides a seamless UX while ensuring the user is the only one who can move their money out.
* **Use case:** Enabling users to "cash out" to the traditional financial world, such as sending funds to their primary bank or paying a bill.

### Asset conversions (on/off-ramps)

This pillar creates a powerful, "closed-loop" economy *within* your platform. It allows users to move seamlessly between their crypto assets (managed by you) and their new fiat balance (managed by UR), all without the friction of external bank transfers.

#### Crypto-to-fiat (off-ramp)

* **What it is:** This is the core "cash out" flow. It allows a user to sell crypto from their balance on your platform and instantly receive fiat in their UR account.
* **How it works:** Your platform requests a quote, the user confirms, and you execute the trade. Our backend performs an atomic settlement: depending on integration mode, UR debits crypto from either the user's wallet (External Wallet Access Mode) or the partner's corporate omnibus account (Managed Custody Mode), and simultaneously credits the corresponding fiat to the end-user's personal UR account.
* **Use case:** This is the primary feature for `Tourist` users, allowing them to off-ramp and see immediate value before completing full KYC.

#### Fiat-to-crypto (on-ramp)

{% hint style="warning" %}
**Available soon.** On-ramp (fiat-to-crypto) is not yet available for integration and will be enabled in a future release. The reference below is provided for preview only.
{% endhint %}

* **What it is:** This is the "closed-loop" on-ramp. It allows a user to buy crypto from you by *using the fiat balance they already hold* in their UR account.
* **How it works:** The user confirms a quote, and your platform calls our API to execute an instant, internal fiat transfer from the user's account to your corporate omnibus account. Once you receive our "success" webhook, you can release the crypto to the user's wallet.
* **Use case:** Creating a frictionless experience, increasing your platform's volume and user engagement by removing the wait time for bank transfers.

### FX (fiat-to-fiat)

Users can convert between fiat currencies they hold (e.g., USD to EUR). UR executes an atomic burn-and-mint operation on-chain, burning one tokenized deposit and minting another in a single transaction.

See [FX conversion](/money-movement/fx) for the detailed flow.

### Internal transfers (P2P)

Instant fiat transfers between two UR users. Because balances are [tokenized deposits](/concepts/tokenized-deposits) on Mantle Network, transfers settle in milliseconds.

### Card programs

Issue co-branded Mastercard debit cards for users to spend their fiat balance. Settlement happens atomically on-chain at point of sale.

See [Tokenized deposits: spending flow](/concepts/tokenized-deposits#the-spending-flow-from-fiat-balance-to-a-real-world-purchase) for how card spending works under the hood.


# Deposits

Fiat bank transfers and crypto-to-fiat offramp flows.

### Fiat deposit via bank transfer

{% hint style="info" %}
This flow also applies to UR Peer Transfers: fiat transfers between 2 `Live` UR accounts.
{% endhint %}

This flow enables a fully verified (`Live`) user to fund their UR account from an external, third-party bank account (e.g., their primary bank).

{% hint style="info" %}
EUR and CHF deposits must come from a bank account in the user's own name (same-name). USD deposits may also come from a third-party account, with additional review; see [USD deposits from a third-party account](#usd-deposits-from-a-third-party-account).
{% endhint %}

<table><thead><tr><th>Flow</th><th width="350.0859375">Details</th><th>Responsibility of</th></tr></thead><tbody><tr><td>Prerequisite: User is <code>Live</code></td><td>The user's status must be <code>Live</code> to access this feature. Your UI should only show the "Deposit" option to <code>Live</code> users.</td><td>Partner</td></tr><tr><td><ol><li>User Clicks "Deposit"</li></ol></td><td>The user selects "Deposit" and chooses "Bank Transfer" from within your app.</td><td>Partner</td></tr><tr><td><ol start="2"><li>Partner Fetches Account Details</li></ol></td><td>Your backend, which must be authorized, calls UR's API to retrieve the user's personal bank account details (IBAN, BIC, Bank Name).</td><td>Partner</td></tr><tr><td><ol start="3"><li>Partner Displays Account Details</li></ol></td><td>Your app's frontend displays the user's unique IBAN and transfer instructions (e.g., "Only send EUR or CHF to this account").</td><td>Partner</td></tr><tr><td><ol start="4"><li>User Initiates External Transfer</li></ol></td><td>The user leaves your app, logs into their external bank app (e.g., UBS, Revolut), and initiates a SEPA/SWIFT transfer to the IBAN provided.</td><td>User</td></tr><tr><td><ol start="5"><li>UR Receives Funds &#x26; Notifies Partner</li></ol></td><td>When the funds arrive (hours or days later), UR's banking core identifies the receiving IBAN, credits the user's account balance, and sends a <code>payment.received</code> webhook to your backend.<br><br>Alternatively, you may monitor on-chain transactions specific to said user.</td><td>UR -> Partner</td></tr><tr><td><ol start="6"><li>Partner Updates User</li></ol></td><td><ul><li>Your backend receives the webhook, verifies the details, and updates the user's balance in your system.</li><li>You should send a push notification or in-app update to inform the user (e.g., "Your €100.00 deposit has arrived.").</li></ul></td><td>Partner</td></tr></tbody></table>

{% hint style="warning" %}
**EUR and CHF use the default IBAN; USD uses a separate one on request.** When a user reaches `Live`, UR automatically issues a personal Swiss IBAN for **EUR and CHF** transfers. UR does not issue a **USD IBAN** by default; you request one when the user wants to receive USD. The EUR/CHF IBAN does not receive USD; show the USD IBAN for USD transfers.

The EUR/CHF IBAN is ready as soon as the user is `Live`, with no request and no prerequisite pay-in. To get the user a USD IBAN, call `POST /v1/apply-usd-payin`. The call is synchronous: if the user is `Live`, UR creates the USD IBAN and returns success, with no manual review and no prior EUR or CHF pay-in required. After a successful call, the USD IBAN appears under the `USD` key of `depositBank` in the user profile; read it from there before showing it to the user. USD deposits from an account not in the user's own name are subject to additional review (see [USD deposits from a third-party account](#usd-deposits-from-a-third-party-account)).

Fetch deposit details from the BR profile (`GET /api/fma/v1/br`), where `depositBank` is keyed by currency. Show the user the IBAN whose currency matches the transfer they will send. Do not show the EUR/CHF IBAN for a USD transfer, or the USD IBAN for a EUR or CHF transfer.
{% endhint %}

```mermaid
sequenceDiagram
participant User (on Partner App)
participant Partner (Frontend)
participant Partner (Backend)
participant UR (Backend)
participant User (at External Bank)
participant UR (Banking Core)

Note over User (on Partner App), UR (Banking Core): Prerequisite: User status is "Live"

User (on Partner App)->>Partner (Frontend): 1. Clicks "Deposit"
Partner (Frontend)->>Partner (Backend): 2. Request Account Details
Partner (Backend)->>UR (Backend): 3. GET /v1/users/{id}/accounts (using auth_token)
UR (Backend)-->>Partner (Backend): 4. Return {"iban": "CH...", "bic": "...", ...}
Partner (Backend)-->>Partner (Frontend): 5. Send Account Details
Partner (Frontend)->>User (on Partner App): 6. Display IBAN & Bank Details

User (on Partner App)-->>User (at External Bank): 7. (User goes to their other bank's app)
User (at External Bank)->>UR (Banking Core): 8. Initiates SEPA/SWIFT transfer

Note over User (at External Bank), UR (Banking Core): ...Hours or days later...

UR (Banking Core)->>UR (Backend): 9. Funds received for user's IBAN
UR (Backend)->>UR (Backend): 10. Credit User's Account Balance
UR (Backend)->>Partner (Backend): 11. Webhook: payment.received (amount, currency, user_id)

Partner (Backend)-->>Partner (Frontend): 12. Update UI (e.g., via WebSocket)
Partner (Frontend)->>User (on Partner App): 13. Show "Deposit Received" & Updated Balance
```

### USD deposits from a third-party account

UR screens inbound USD by who sent it:

* **Same-name sender:** the deposit comes from an account in the user's own name. UR credits it to the user's UR account with no hold.
* **Any other sender:** UR holds the deposit for up to 7 days for review. On release, UR deducts a fixed **USD 50** fee and credits the remainder to the user's UR account; UR does not return the funds to the sending bank. If the review does not pass, UR does not release the funds.

This review applies to USD only; EUR and CHF deposits must always come from a same-name account. Tell your users to send USD from an account in their own name to avoid the hold and the fee.

### Crypto-to-fiat conversion (off-ramp)

<table><thead><tr><th width="189.90625">Flow</th><th width="371.5859375">Details</th><th>Responsibility of</th></tr></thead><tbody><tr><td>1. Initiate Off-ramp</td><td>The user taps "Cash Out" for 1 ETH on your app.</td><td>Partner</td></tr><tr><td>2. Internal Debit</td><td>Your backend deducts 1 ETH from the user's custodial balance.</td><td>Partner</td></tr><tr><td>3. Request Quote</td><td>Your backend calls the liquidity provider API to request a real-time conversion quote for 1 ETH to EUR.</td><td>Partner</td></tr><tr><td>4. Source Liquidity</td><td>The liquidity provider API gets a live, executable price.</td><td>UR / Partner</td></tr><tr><td>5. Provide Quote</td><td>The liquidity provider API returns a firm, time-limited quote (e.g., "€1900.00, valid for 30s") to your backend.</td><td>UR -> Partner</td></tr><tr><td>6. Confirm Quote</td><td>Your frontend displays the quote, and the user taps "Confirm."</td><td>Partner</td></tr><tr><td>7. Execute Trade</td><td>Your backend calls the liquidity provider API with the unique <code>quote_id</code> to execute the trade.</td><td>Partner</td></tr><tr><td>8. Settle Trade</td><td>UR performs an atomic sequence:a. Debits 1 ETH from <em>Partner's</em> corporate crypto account.b. Sells 1 ETH via the liquidity provider.c. Credits the resulting €1900.00 to the <em>end-user's</em> UR account.</td><td>UR</td></tr><tr><td>9. Notify User</td><td>UR sends a <code>payment.received</code> webhook. Your backend updates the user's UI and sends a push notification.</td><td>UR -> Partner -> User</td></tr></tbody></table>

```mermaid
sequenceDiagram
participant User on Partner Platform
participant Partner Platform Backend
participant UR API
participant Liquidity Provider API

User on Partner Platform->> Partner Platform Backend: 1. Initiates "Cash Out" (e.g., 1,000 USDC)
activate Partner Platform Backend

Partner Platform Backend->> Partner Platform Backend: 2. Debits User's Custodial Balance

Partner Platform Backend->>UR API: 3. Requests Quote for 1,000 USDC -> EUR
activate UR API

UR API->>Liquidity Provider API: 4. Gets Live Price
activate Liquidity Provider API
Liquidity Provider API-->>UR API: 5. Returns Price
deactivate Liquidity Provider API

UR API-->> Partner Platform Backend: 6. Returns Firm Quote (Expires in 30s)
deactivate UR API
deactivate Partner Platform Backend

User on Partner Platform->> Partner Platform Backend: 7. Confirms Quote in UI
activate Partner Platform Backend

Partner Platform Backend->>UR API: 8. Executes Trade with Quote ID
activate UR API

note right of UR API: 9. Executes Atomic Sequence
UR API->>UR API: a) Debits Partner Platform's USDC Acct
UR API->>Liquidity Provider API: b) Sells USDC for EUR
UR API->>UR API: c) Credits User's UR EUR Acct

UR API-->>User on Partner Platform: 10. Sends Notification: "€863.00 deposited"
deactivate UR API
deactivate Partner Platform Backend
```


# Withdrawals

Bank payouts and fiat-to-crypto onramp flows.

### Fiat payout

{% hint style="info" %}
This flow also applies to UR Peer Transfers: fiat transfers between 2 `Live` UR accounts.
{% endhint %}

This flow enables a fully verified (`Live`) user to send funds from their UR account to an external bank account. This is a high-security action that requires the user to authenticate and sign the transaction.

<table><thead><tr><th width="213.1640625">Flow</th><th width="431.45703125">Details</th><th>Responsibility of</th></tr></thead><tbody><tr><td>1. Prerequisite</td><td>The user's status must be <code>Live</code>.</td><td>Partner</td></tr><tr><td>2. Initiate Payout</td><td>The user clicks "Withdraw" or "Send to Bank" from within your app.</td><td>Partner</td></tr><tr><td>3. Request Payout URL</td><td>Your backend calls <code>GET /v1/payouts/url</code> to get a one-time URL for the UR Payout Webview.</td><td>Partner</td></tr><tr><td>4. Open Payout Webview</td><td>Your app opens the UR Payout Webview with the URL provided by UR.</td><td>Partner</td></tr><tr><td>5. User Authenticates</td><td>Inside the UR Webview, the user <em>must</em> re-authenticate (via Email/Google SSO) to securely access their private key.</td><td>UR</td></tr><tr><td>6. Complete &#x26; Confirm</td><td>The user fills in the payout details (amount, destination IBAN, reference) and confirms the transaction.</td><td>UR</td></tr><tr><td>7. Authorize &#x26; Process</td><td>The user's confirmation, combined with their authentication, authorizes UR's backend to execute the on-chain payout.</td><td>UR</td></tr><tr><td>8. Notify Partner</td><td>UR's backend executes the on-chain payout, and the funds are sent. UR sends a <code>payment.sent</code> webhook to your backend.</td><td>UR -> Partner</td></tr><tr><td>9. Update User</td><td>Your backend receives the webhook and updates the user's balance and transaction history in your app.</td><td>Partner</td></tr></tbody></table>

```mermaid
sequenceDiagram
participant User (on Partner App)
participant Partner (Frontend)
participant Partner (Backend)
participant UR (Webview - Payout)
participant UR (Backend)
participant UR (Embedded Wallet)
participant UR (Banking Core)

Note over User (on Partner App), UR (Banking Core): Prerequisite: User status is "Live"

User (on Partner App)->>Partner (Frontend): 1. Clicks "Withdraw"
Partner (Frontend)->>Partner (Backend): 2. Request Payout URL
Partner (Backend)->>UR (Backend): 3. GET /v1/payouts/url
UR (Backend)-->>Partner (Backend): 4. Return one-time Payout URL
Partner (Backend)-->>Partner (Frontend): 5. Send URL
Partner (Frontend)->>User (on Partner App): 6. Open UR Payout Webview

User (on Partner App)->>UR (Webview - Payout): 7. Authenticate (Email/Google SSO)
UR (Webview - Payout)->>UR (Embedded Wallet): 8. Securely unlock private key

User (on Partner App)->>UR (Webview - Payout): 9. Enters Payout Details (Amount, IBAN)
UR (Webview - Payout)->>User (on Partner App): 10. Show Confirmation Screen
User (on Partner App)->>UR (Webview - Payout): 11. Clicks "Confirm & Send"

UR (Webview - Payout)->>UR (Backend): 12. Submit Payout Request

note right of UR (Backend): 13. User's auth (Step 7) authorizes this
UR (Backend)->>UR (Embedded Wallet): 14. Generate Signed Transaction
UR (Embedded Wallet)-->>UR (Backend): 15. Return Signed Tx

UR (Backend)->>UR (Banking Core): 16. Execute SEPA/SWIFT Transfer (submits tx)

UR (Banking Core)-->>UR (Backend): 17. Payout Successful
UR (Backend)->>Partner (Backend): 18. Webhook: payment.sent (user_id, amount)

Partner (Backend)-->>Partner (Frontend): 19. Update UI (e.g., via WebSocket)
Partner (Frontend)->>User (on Partner App): 20. Show "Withdrawal Sent" & Updated Balance
```

### Fiat-to-crypto conversion (on-ramp)

{% hint style="warning" %}
**Available soon.** On-ramp (fiat-to-crypto) is not yet available for integration and will be enabled in a future release. The reference below is provided for preview only.
{% endhint %}

{% hint style="info" %}
This flow allows a user to instantly purchase crypto from your platform using the fiat balance held in their UR account.
{% endhint %}

**Scenario:** A user has $1,000 in their embedded UR account and wants to on-ramp $500 to buy USDC.

<table><thead><tr><th width="164.703125">Flow</th><th width="420.93359375">Details</th><th>Responsibility of</th></tr></thead><tbody><tr><td>1. Initiate On-Ramp</td><td>The user goes to the "Buy Crypto" section and chooses to pay using their available fiat balance in the embedded UR account.</td><td>Partner</td></tr><tr><td>2. Confirm Transaction</td><td>Partner's trading engine provides a real-time quote for the crypto purchase. The user confirms the amount.</td><td>Partner</td></tr><tr><td>3. Instant Fiat Settlement</td><td>Partner execute an internal transfer of $500 from the user's individual UR account to Partner's corporate UR account, enabled by the Transfer Assets flow. The settlement is instant.</td><td>Partner -> UR</td></tr><tr><td>4. Crypto Delivery</td><td>Upon confirmation of the instant fiat settlement, Partner's trading engine is triggered to purchase the crypto and deliver it to the user's connected wallet.</td><td>Partner</td></tr></tbody></table>

```mermaid
sequenceDiagram
participant User
participant Partner
participant UR

User->>Partner: 1. Initiate On-Ramp (e.g., $500 to USDC)
Note over User, Partner: User selects their existing fiat balance as the payment source.
Partner ->>Partner : 2. Get Crypto Quote & User Confirms
Partner ->>UR: 3. Call `Payments API` (Debit $500 from the User)
UR ->>UR: 4. Settle Fiat Instantly (Internal Transfer)
UR -->>Partner: 5. Fiat Settlement Confirmed
Partner ->>Partner: 6. Execute Crypto Purchase
Partner -->>User: 7. Deliver Crypto to User's Wallet
```


# FX conversion

Convert between currencies using atomic burn-and-mint operations.

{% hint style="warning" %}
Detailed integration guides for FX will be added as the Managed Custody Mode FX API becomes available.
{% endhint %}

## What this will cover

* How FX conversion works (atomic burn-and-mint on Mantle Network)
* Supported currency pairs across 7 tokenized deposit currencies
* Rate calculation and fees
* Step-by-step integration guide with code samples

{% hint style="info" %}
FX is already available for **External Wallet Access Mode** integrations. See the [External Wallet Access Mode FX flow](/integration-methods/external-wallet-access-mode#fiat-to-fiat-fx) for the current implementation with sequence diagrams.
{% endhint %}

## How FX works

Users can instantly convert between fiat currencies they hold (e.g., USD to EUR). Behind the scenes, UR executes an atomic burn-and-mint operation on the user's tokenized deposits.

```mermaid
sequenceDiagram
participant U as User
participant App as UR App
participant UR as UR (System)
participant MN as Mantle Network

U->>App: 1. Taps 'Convert'
activate App
U->>App: 2. Enters "100 USD to EUR"

App->>UR: 3. Request FX Quote (USD/EUR)
activate UR
UR-->>App: 4. Provide Live Rate (e.g., 1 USD = 0.92 EUR)

App->>U: 5. Display Quote: "Convert 100 USD to ~92.00 EUR?"
U->>App: 6. Confirms Conversion

App->>UR: 7. Execute Confirmed FX Trade

UR-->>MN: 8. Initiate Atomic Swap
activate MN
Note over UR, MN: The following is a single atomic transaction
UR->>MN: 8a. BURN 100 USD24 from User's Wallet
UR->>MN: 8b. MINT 92.00 EUR24 to User's Wallet

MN-->>UR: 9. Transaction Confirmed
deactivate MN

UR-->>App: 10. Conversion Successful
deactivate UR

App->>U: 11. Show success screen & updated balances
deactivate App
```


# Transfer assets to partner platform

Move funds between user UR accounts and your platform.

If the user has granted write access to the Partner, the Partner will be able to allow users to natively trigger transfers (e.g. "Transfer from UR to \<Partner> account") natively within the partner's UI.

Use case: When users want to transfer fiat from their UR account to their custodial fiat balance within the Partner's platform.

<figure><img src="/files/5JU3ZFZ6mZjmDV4sz6Df" alt=""><figcaption></figcaption></figure>


# URID

The on-chain identity that unlocks UR accounts.

The URID is UR's on-chain identity system. Think of it as a user ID, but represented as an NFT on Mantle Network. Every user and corporate entity that passes KYC/KYB verification receives a URID minted to their wallet address.

## What a URID does

A URID binds a verified real-world identity to an on-chain address. It links together:

| Component             | Description                                                                                          |
| --------------------- | ---------------------------------------------------------------------------------------------------- |
| Compliance status     | KYC/KYB verification result, so the system knows this address belongs to a verified person or entity |
| Account permissions   | What the holder can do (e.g., transact, hold specific currencies, grant write access to partners)    |
| IBAN assignment       | The personal Swiss IBAN associated with this identity                                                |
| Partner relationships | Which platforms have been granted delegated access                                                   |

A valid URID is required before a user can access any UR Account functionality, including deposits, withdrawals, card spending, and FX conversions.

## Corporate vs personal URIDs

| Type           | Issued after | Purpose                                                                                             |
| -------------- | ------------ | --------------------------------------------------------------------------------------------------- |
| Personal URID  | KYC          | Unlocks the user's [UR Account](/concepts/ur-account)                                               |
| Corporate URID | KYB          | Required for managing fund flows between your platform and funds on UR held on behalf of your users |

## Key properties

| Property               | Description                                                                                        |
| ---------------------- | -------------------------------------------------------------------------------------------------- |
| Non-transferable       | A URID is soulbound to the wallet it was minted to. It cannot be sold or moved.                    |
| Revocable              | If compliance status changes, the URID can be deactivated, immediately freezing account access.    |
| On-chain verifiability | Any smart contract can check whether an address holds a valid URID before executing a transaction. |


# UR Account

A hybrid financial account bridging the regulated financial system with on-chain settlement.

The UR Account is a hybrid financial infrastructure that bridges the regulated financial system (via UR's regulated entity) with Web3 settlement rails. It functions as the user's regulated fiat account linked to an active [URID](/concepts/urid).

Fiat balances are issued as [tokenized deposits](/concepts/tokenized-deposits) (e.g., USD24, EUR24) on Mantle Network, backed 1:1 by fiat reserves held by UR's regulated entity. This architecture provides users with a fully compliant account interface (including personal Swiss IBANs and Mastercard debit card capabilities) while enabling instant, atomic on-chain settlement for crypto-to-fiat conversions, card spending, and cross-border transfers.

## How a UR Account holds assets

Every UR Account is backed by an on-chain wallet address on Mantle Network. Tokenized deposits (e.g., USD24, EUR24) are ERC-20 tokens held at this address.

How assets are split depends on the integration mode:

* **Managed Custody Mode**: UR manages the user's fiat account (tokenized deposits, IBAN, card). The UR-managed account holds fiat only; the user's crypto is always held in a separate external (non-UR) wallet (partner-side or the user's own). Crypto enters the UR Account only via off-ramp, which credits fiat.
* **External Wallet Access Mode**: Both fiat (tokenized deposits) and crypto live in the user's single external wallet. The UR Account and the user's wallet are the same address.
* **Delegated Mode**: Similar to Managed Custody Mode; UR manages the wallet via delegated smart contracts. The user performs a one-time delegation during onboarding.

{% hint style="warning" %}
Delegated Mode will be deprecated soon and is being replaced by Managed Custody Mode. New partners should use Managed Custody Mode or External Wallet Access Mode.
{% endhint %}

In all three modes, the UR Account provides a regulated Swiss IBAN account issued by UR's regulated entity (for fiat rails, IBAN, and card), with on-chain settlement for all operations.

## What a UR Account includes

* **Personal Swiss IBAN**: receive SEPA Instant and SWIFT transfers directly
* **Multi-currency balances**: hold EUR, USD, CHF, CNH, SGD, JPY, and HKD as tokenized deposits
* **Mastercard debit card**: spend fiat balances anywhere Mastercard is accepted; settlement happens on-chain in milliseconds
* **Instant FX**: convert between supported currencies atomically (burn one token, mint another in a single transaction)
* **On-chain transparency**: balances and transactions are recorded on Mantle Network, fully auditable

## Account structure by integration mode

How the account is provisioned depends on how your platform integrates with UR:

* [**Managed Custody Mode**](/integration-methods/managed-custody-mode) (recommended): UR manages a fiat account per user (tokenized deposits, IBAN, card). The UR-managed account holds fiat only; the user's crypto is always held in a separate external (non-UR) wallet (partner-side or the user's own). The partner backend orchestrates all fiat actions via API.
* [**External Wallet Access Mode**](/integration-methods/external-wallet-access-mode): Users connect their own wallet (e.g., MetaMask, SafePal). The URID is minted directly to their address, and they interact with UR through your UI or directly on-chain.
* [**Delegated Mode**](/integration-methods/delegated-mode): Legacy API-driven integration via delegated smart contracts. Existing partners on this mode continue to be supported, but the mode will be deprecated soon (see the note above).


# Tokenized deposits

1:1 fiat-backed tokens on Mantle Network.

The core that powers UR is the concept of tokenized deposits. A tokenized deposit is a one-to-one, fully-backed digital representation of fiat money held by UR's regulated entity, recorded on the Mantle Network.

For example, when a user holds 100 USD24 tokens in their UR wallet, they have a legal claim to $100 USD. The token is not a stablecoin or a synthetic asset. It is a deposit receipt issued by UR's regulated entity: a direct, one-to-one claim on fiat that the entity holds in full reserve.

## Why tokenize deposits?

Representing fiat balances as on-chain tokens unlocks capabilities that traditional banking infrastructure cannot offer:

* **Instant settlement**: transfers, FX conversions, and card spending settle in milliseconds, not days. When a user taps their Mastercard, UR burns the corresponding tokens and approves the transaction atomically.
* **Atomic operations**: FX conversion is a single on-chain transaction (burn one currency token, mint another). No intermediary holding accounts, no T+1 settlement.
* **Composability**: because balances are standard tokens on Mantle Network, they can interact with smart contracts, enabling programmable finance (e.g., automated savings, partner-triggered pull payments).
* **Transparency**: balances and transaction history are on-chain and auditable. No reconciliation lag between what the user sees and what the system records.

## Supported currencies

UR issues tokenized deposits in seven currencies:

| Token | Currency                |
| ----- | ----------------------- |
| EUR24 | Euro                    |
| USD24 | US Dollar               |
| CHF24 | Swiss Franc             |
| CNH24 | Chinese Yuan (offshore) |
| SGD24 | Singapore Dollar        |
| JPY24 | Japanese Yen            |
| HKD24 | Hong Kong Dollar        |

Each token is minted when fiat is deposited and burned when fiat is withdrawn. The token is not backed by a separate reserve; it *is* the deposit, a direct liability on the issuing entity's balance sheet.

## User experience

On the UI, tokenized deposits are displayed as fiat balances. Users see "€100.00" rather than "100 EUR24". The on-chain mechanics are invisible unless the user chooses to inspect their wallet directly.

## The off-ramping flow: from crypto to fiat balance

This is the process of a user converting their digital assets (e.g., USDC) into spendable fiat balance in UR.

<figure><img src="/files/b6fDwcGgiCcv5VygLQ0g" alt=""><figcaption></figcaption></figure>

## The spending flow: from fiat balance to a real-world purchase

This is how a user spends their tokenized deposit balance using the UR Mastercard. UR instantly burns the corresponding tokens and approves the transaction, and we manage the settlement with Mastercard rails later on.

```mermaid
sequenceDiagram
participant C as Cardholder
participant M as Merchant POS
participant AB as Acquiring Bank
participant MC as Mastercard Network
participant UR as UR
participant MN as Mantle Network

C->>M: 1. Taps UR Mastercard to pay $50
activate M
M->>AB: 2. Authorization Request
activate AB
AB->>MC: 3. Forward Request
activate MC
MC->>UR: 4. Route Request: "Authorize $50 for Card XYZ?"
activate UR

UR->>MN: 5. Query Balance of User's Wallet
activate MN
MN-->>UR: 6. Return Balance (e.g., 1000 USD24)
deactivate MN

UR-->>UR: 7. Verify Funds & Perform Fraud Checks

alt Sufficient Funds & Checks Pass
    UR->>MN: 8. Instruct: "BURN 50 USD24 from User's Wallet"
    activate MN
    MN-->>UR: 9. Burn Confirmed. New Balance: 950 USD24
    deactivate MN
    UR-->>MC: 10. Send Response: APPROVED
else Insufficient Funds or Fraud Flag
    UR-->>MC: 10. Send Response: DECLINED
end

deactivate UR
MC-->>AB: 11. Forward Response
deactivate MC
AB-->>M: 12. Send Final Response
deactivate AB
M-->>C: 13. Transaction Result
deactivate M
Note right of M: Displays "Approved" / "Declined" on screen
```


# On-chain ledger

How UR records every transaction on Mantle Network, and why it matters for partners.

Every UR transaction (deposits, withdrawals, FX conversions, card spending, transfers) is recorded on the Mantle Network. This on-chain ledger is UR's core infrastructure layer: it replaces the traditional bank ledger with a transparent, real-time, programmable record of all account activity.

## How it works

When a user performs any financial action in UR, the system executes it as an on-chain transaction:

* **Deposit**: fiat arrives via SEPA/SWIFT → UR mints the corresponding tokenized deposit (e.g., 100 EUR24) to the user's wallet
* **Withdrawal**: UR burns the tokens → fiat is sent out via SEPA/SWIFT
* **FX conversion**: burn one currency token, mint another in a single atomic transaction
* **Card spend**: burn tokens instantly at point of sale, settle with Mastercard later
* **Transfer**: move tokens between wallets on-chain

Each operation is a verifiable, immutable record on Mantle Network.

## Comparison to traditional bank ledgers

Traditional banks maintain internal databases that only they can read. Reconciliation between systems (bank, card processor, partner) takes hours or days, and discrepancies are common.

|                     | Traditional bank ledger   | UR on-chain ledger                  |
| ------------------- | ------------------------- | ----------------------------------- |
| **Visibility**      | Bank-internal only        | Public, auditable by anyone         |
| **Settlement**      | T+1 to T+3                | Instant (block confirmation)        |
| **Reconciliation**  | Manual, periodic batches  | Unnecessary; single source of truth |
| **Auditability**    | Requires bank cooperation | Read directly from chain            |
| **Programmability** | None                      | Smart contract composability        |

## Reading on-chain state

Partners can query the Mantle Network directly to:

| Action                   | What you can do                                                   |
| ------------------------ | ----------------------------------------------------------------- |
| **Verify balances**      | Read token balances for any UR wallet without calling the UR API  |
| **Monitor transactions** | Subscribe to on-chain events for real-time notifications          |
| **Build reports**        | Pull historical transaction data for compliance or reconciliation |
| **Audit**                | Independently verify that user balances match expected state      |

This works with any EVM-compatible tooling (ethers.js, viem, Foundry, block explorers).

## Why this matters

The on-chain ledger is UR's key differentiator:

| Benefit                     | Detail                                                                                                                                          |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **No reconciliation lag**   | What the user sees, what the partner queries, and what the regulator audits is the same data, in real time                                      |
| **Partner independence**    | You don't need to trust UR's reporting; you can verify on-chain                                                                                 |
| **Composability**           | Balances are standard ERC-20 tokens. Partners can build programmable finance on top (automated sweeps, conditional payments, DeFi integrations) |
| **Regulatory transparency** | Regulators can audit the ledger directly, reducing compliance overhead for everyone in the chain                                                |


# The user lifecycle

The URID account states and how a single KYC flow moves a user from Tourist to Live.

To access the full range of core banking operations, a user must hold a [URID](/concepts/urid) in the `Live` state. Onboarding is a single KYC flow that takes a newly minted URID from `Tourist` (minted, before KYC) to `Live` (KYC passed). This page describes the account states, how a user moves between them, and what your platform is responsible for along the way.

For what UR verifies during KYC and the methods it uses (questionnaire, proof of address, NFC scan, liveness, AML screening), see [KYC & compliance](/concepts/kyc-and-compliance).

## User states

Every URID carries an on-chain account status, and that status controls what the user can do. The values are the `AccountStatusV2` enum; for the on-chain mapping, see [Smart contracts](https://docs.ur.app/api-reference/smart-contracts). The states are the following:

| Status        | Value | How the user reaches it                                        | What the user can do                                                                                              |
| ------------- | ----- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `Na`          | 0     | New user; URID not yet minted                                  | Nothing yet                                                                                                       |
| `Tourist`     | 2     | URID is minted, before KYC                                     | Receive partner off-ramp and inbound P2P (up to 250 USD/EUR); cannot move funds externally, hold an IBAN, or send |
| `Live`        | 5     | Completes KYC and passes compliance review                     | All banking features: external deposits (IBAN), external payouts, send and receive P2P, FX, card                  |
| `SoftBlocked` | 1     | An ongoing task (CRS, ongoing KYC, or EDD) passed its deadline | Money features locked until the open task is completed; recoverable to `Live`                                     |
| `Blocked`     | 3     | A compliance action freezes the account                        | Account frozen                                                                                                    |
| `Closed`      | 4     | The account is closed                                          | Terminal; no access                                                                                               |

{% hint style="info" %}
`Tourist` is not a KYC stage the user completes. It is the state a URID is minted into before KYC. There is one KYC flow, and passing it moves the user straight to `Live`. Functions for any non-`Live` status are restricted on-chain.
{% endhint %}

## State transitions

A URID is always minted in `Tourist` and promoted to `Live` once KYC passes; there is no direct path from `Na` to `Live`.

```mermaid
flowchart LR
    Na["Na (0)"] -->|mint URID| Tourist["Tourist (2)"]
    Tourist -->|KYC passed| Live["Live (5)"]
    Live -->|ongoing task overdue| SoftBlocked["SoftBlocked (1)"]
    SoftBlocked -->|task completed| Live
    Live -->|compliance action| Blocked["Blocked (3)"]
    Blocked --> Closed["Closed (4)"]
```

A user whose KYC is rejected stays in `Tourist` and cannot access core banking features. `Blocked` and `Closed` can also be reached from other states by a compliance action.

## What you handle vs UR

Onboarding responsibilities split as follows:

| Step             | UR handles                                                           | You handle                                                                          |
| ---------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| KYC verification | Identity checks, screening, compliance review                        | Presenting KYC components in your app (UR webview or Sumsub SDK)                    |
| Wallet creation  | Minting the URID NFT, provisioning the wallet (Managed Custody Mode) | Mapping your user ID to their URID                                                  |
| State changes    | Transitioning the URID between statuses                              | Listening for webhooks, updating your UI accordingly                                |
| KYC prompt       | Nothing; you decide when                                             | Prompting the user to complete KYC when they attempt an action that requires `Live` |

## The onboarding flow

A URID moves from `Tourist` to `Live` through a single KYC flow. The steps run in this order:

{% stepper %}
{% step %}

### URID is minted (Tourist)

The user authenticates, and UR mints the URID NFT in `Tourist` status. The user can receive partner off-ramp and inbound P2P (up to 250 USD/EUR), but cannot move funds externally.
{% endstep %}

{% step %}

### Proof of address

The user confirms their residential address through the Sumsub flow (GPS method).
{% endstep %}

{% step %}

### Questionnaire

The user answers the questionnaire (eligibility, source of funds, declarations) through the Sumsub flow.
{% endstep %}

{% step %}

### Proof of identity

The user completes the passport or national ID NFC scan and a liveness check through the Sumsub flow. See [KYC components](#kyc-components) for the mobile NFC requirement.
{% endstep %}

{% step %}

### Form A signing

Outside Sumsub, the user signs the Beneficial Ownership Declaration (Form A) with the embedded wallet that holds their URID.
{% endstep %}

{% step %}

### Compliance review

UR screens the user (sanctions, PEP, adverse media) and reviews the submitted data. On pass, the URID moves to `Live` and all account restrictions are lifted automatically. On failure, the attempt is rejected and the user stays in `Tourist`.
{% endstep %}
{% endstepper %}

Your backend tracks progress through a KYC status field whose exact values depend on the integration path: `kycFlow.currentStep` on the UR main stack (see the [External Wallet Access Mode API reference](https://docs.ur.app/api-reference/account/external-wallet-access-mode)), or the session `state` in the partner-managed SDK path (see the [Managed Custody SDK KYC reference](https://docs.ur.app/api-reference/kyc-and-kyb/managed-custody-sdk-kyc)).

### Webhooks you receive

You receive the following onboarding webhooks. For money-movement and other events, see the full [event catalog](/developer-resources/webhook#6-event-catalog).

| Webhook event                  | When                                                                                           | Applies to                     |
| ------------------------------ | ---------------------------------------------------------------------------------------------- | ------------------------------ |
| `sumsub_kyc_result`            | A Sumsub review completes (`Pass` or `Rejected`)                                               | All KYC paths                  |
| `kyc_status`                   | UR returns the compliance decision (`Pending`, `Pass`, `Rejected`, `ManualReview`, or `Error`) | All KYC paths                  |
| `fma.account.result`           | Partner-managed onboarding ends (`activated` or `rejected`)                                    | Partner-managed (FMA) KYC only |
| `fma.penny_drop.result`        | A penny-drop attempt resolves (`verified` or `failed`)                                         | Partner-managed (FMA) KYC only |
| `fma.additional_kyc.required`  | UR ops request a partner-driven retry of part or all of a user's KYC                           | Partner-managed (FMA) KYC only |
| `fma.additional_kyc.completed` | A retry session reaches its last step                                                          | Partner-managed (FMA) KYC only |

{% hint style="info" %}
The definitive `Tourist → Live` signal is `kyc_status` with `status: "Pass"` on the UR main stack, or `fma.account.result` with `status: "activated"` for partner-managed KYC. Only raise the user's access level on that signal.
{% endhint %}

## KYC components

The KYC flow collects four components, presented in the order shown in [The onboarding flow](#the-onboarding-flow). Depending on your integration mode, you present them through the UR webview or the Sumsub SDK.

**Proof of address.** UR uses the GPS method to confirm the user's residential address. UR can show this through a webview or iFrame pointing to the UR web app (get.ur.app). The user must be signed in.

**Questionnaire.** Questions about the user, including eligibility screening, source of income, and monthly income. For a lightweight integration, UR can show the questionnaire through a webview or iFrame pointing to get.ur.app. The user must be signed in.

**Proof of identity (passport or national ID NFC scan).**

{% hint style="warning" %}
**NFC scanning is only available on a mobile app.** If your platform uses the Sumsub SDK directly (External Wallet Access Mode), the NFC scan step is only supported in the Sumsub mobile SDK (iOS/Android); it is not available in the Sumsub web SDK. You must expose this step through your mobile app. Web-only platforms cannot complete this step through the Sumsub SDK. If your platform has no mobile app, email <support@ur.app> to discuss alternatives.
{% endhint %}

* Scenario A1 (iOS): UR returns a QR code and link to its NFC scanning provider. When the user taps it, an iOS App Clip opens, the user completes the NFC scan, then returns to your app.
* Scenario A2 (Android): UR returns a QR code and link to its NFC scanning provider. The user downloads a separate app to perform the NFC scan, then returns to your app.

**Agreement (Form A) signing.** Form A contains the information the user filled in during KYC, so the user must complete the proof of address and questionnaire first and the data must exist in UR's database before UR can generate the form. The user signs Form A with the embedded wallet that holds their URID. UR can show Form A through a webview pointing to get.ur.app. The user must be signed in.


# KYC & compliance

What UR verifies during KYC, the data we collect, and the methods we use.

UR is a regulated financial product. Every end user must complete **Know Your Customer (KYC)** checks before they can access core banking features such as external transfers, IBANs, debit cards, and FX. This page explains **what** UR verifies, the data UR collects, and **how** each check works. It lists the data fields and the options behind each check, but not the exact user-facing question wording, which UR's compliance team configures and shows to users at runtime through the Sumsub SDK.

For the user-journey view of when each step happens, see [The User Lifecycle](/concepts/overview-the-user-lifecycle). For implementation details, including endpoints, status enums, and webhooks, see the [Developer Resources](https://docs.ur.app/api-reference/account/external-wallet-access-mode) section.

## What UR verifies

UR verifies five core areas: **identity**, **domicile**, **account purpose and source of funds**, **beneficial ownership**, and **sanctions exposure**. Each area is collected through a dedicated check described below.

### Data conventions

UR applies the following conventions to all KYC data:

* **Eligibility basis:** UR determines eligibility by the user's country of residence (domicile). A small set of restricted nationalities (US persons, North Korea, Iran, Russia) is excluded regardless of residence. See [Supported regions](/getting-started/supported-regions) for the unsupported-residence list and the restricted nationalities.
* **Language:** all data shared with compliance must be in English. For users from non-English-speaking countries, personal-data fields such as name and address are collected in both native script and Latin transliteration.
* **Country codes:** ISO 3166-1 alpha-3 (for example, `POL`, `CHN`, `DEU`, `CHE`).
* **Dates:** `YYYY-MM-DD`.

### Identity verification

UR offers three identity-verification methods. NFC is the primary path. Penny transfer and video verification support users who cannot complete an NFC scan.

| Method                 | Role                       | User device                 |
| ---------------------- | -------------------------- | --------------------------- |
| **NFC + liveness**     | Primary                    | NFC-capable mobile device   |
| **Penny transfer**     | Fallback for EU/FATF users | Any (bank account required) |
| **Video verification** | Reserved fallback          | Any (camera required)       |

#### NFC scan + liveness check *(primary)*

The user scans the electronic chip in their biometric passport or national ID through the Sumsub mobile SDK. UR validates the chip data against the document's certificate chain and reads the identity details directly from the chip, including document number, issuing country, name, date of birth, gender, expiry date, MRZ, and photo. The document must be valid for at least 3 months from the date of submission; UR blocks a document that expires sooner or has already expired. The extracted name, date of birth, and nationality must match the questionnaire responses.

The user then completes a **liveness check**, a short selfie capture in the SDK that confirms the person is physically present. The live photo is matched against the photo retrieved from the NFC chip. Verification only passes when both the liveness check and the photo match succeed.

NFC scanning requires an NFC-capable mobile device. Web-only platforms cannot complete this step through the Sumsub web SDK. See the note in [The User Lifecycle](/concepts/overview-the-user-lifecycle).

In the **UR-hosted webview** integration path, the NFC chip scan, document scan, and selfie are completed in **ReadID** (the ReadID app on Android, or an [App Clip](https://developer.apple.com/app-clips/) on iOS), which UR orchestrates for you. The questionnaire and the domicile (device location) check run in the webview itself. An NFC-capable mobile device is still required for the ReadID step; the webview path removes the partner's need to build the NFC handoff, not the NFC scan itself.

#### Penny transfer *(fallback for FATF-jurisdiction users)*

For users who cannot complete an NFC scan, UR supports a **penny transfer**. The user sends a small bank transfer from their personal bank account to their UR IBAN. UR validates the inbound transfer details and uses them as proof of identity, together with the standard liveness check.

**Constraints**

| Constraint      | Detail                                                                                                                                                                                                                                               |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Same name       | Sender name must match the UR account holder. Family-member or business accounts are not accepted.                                                                                                                                                   |
| FATF country    | The sender's bank must be in one of the 37 active FATF member jurisdictions, determined from the BIC/SWIFT country code.                                                                                                                             |
| Currency        | The inbound penny remittance is EUR (primary) or CHF. (A USD virtual IBAN is not required beforehand: for eligible partners the USD vIBAN is provisioned for `Tourist` users **before** KYC completes, so a USD IBAN is not gated on finishing KYC.) |
| Amount          | Approximately 50 CHF or EUR is recommended; amounts up to 500 CHF or EUR are accepted. There is no enforced minimum; any received amount can be used for validation. Larger transfers may be subject to post-activation account limits.              |
| Processing time | Typically 1 to 2 business days.                                                                                                                                                                                                                      |

{% hint style="info" %}
**Eligibility.** Penny transfer is only available to users whose bank is in a FATF jurisdiction. Users outside that scope are routed to the NFC flow.
{% endhint %}

#### Video verification *(reserved fallback)*

For users who cannot complete either NFC or penny transfer, for example because they have no NFC-capable device and no FATF-jurisdiction bank account, UR offers a video-verification path coordinated case by case with the support team. Contact <support@ur.app> when a user reaches this state.

### Domicile verification (proof of address)

| Method              | Role        | How it works                                                                                                                                                                                |
| ------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **GPS check**       | Primary     | The user confirms that they are at their declared home address. The device captures GPS coordinates, which UR reverse-geocodes and compares with the address provided in the questionnaire. |
| **Document upload** | Alternative | When GPS is not viable, the user uploads a document proving residential address.                                                                                                            |

The GPS check passes when the device location is within the expected proximity of the declared address.

| Constraint    | Accepted                                   |
| ------------- | ------------------------------------------ |
| Document type | Government-issued documents, utility bills |
| Document age  | Issued within the last 3 months            |
| Languages     | English, German, French, or Italian        |

### Account questionnaire

The questionnaire collects the information UR needs to operate a regulated Swiss IBAN account. The categories are listed below. The canonical question set is configured in Sumsub and presented to the user at runtime.

* **Eligibility:** age (18+), citizenship in non-restricted jurisdictions, US Person status, PEP (politically exposed person) declaration, and acceptance of UR's Terms of Use, Privacy Policy, and Card Terms.
* **Personal information:** legal name, date of birth, gender, nationality, residential address, and verified email.
* **Employment:** employment status (employed, self-employed or freelancer, retired, student or trainee, unemployed); job category when employed (employee, manager, C-level or executive, director or board member); and business sector.
* **Account purpose:** the user's intended use of the UR account, such as crypto off-ramp, savings, or investment.
* **Source of funds:** gross annual income range (under 50,000; 50,000 to 100,000; 100,000 to 500,000; 500,000 to 1,000,000; over 1,000,000 USD), approximate total assets range (under 100,000; 100,000 to 500,000; 500,000 to 1,000,000; over 1,000,000 USD), and source-of-funds category (business income or salary, savings and pension, investments, inheritance, or other).
* **Declarations:** address-accuracy declaration, the FINMA-required acknowledgment that deposits are not covered by any depositor-protection scheme, and other regulator-required attestations. The user must confirm UR-specific declarations explicitly; they cannot be auto-filled.

### Beneficial ownership declaration (Form A)

A **Beneficial Ownership Declaration (Form A)** identifies the natural person or persons who ultimately own or benefit from the assets held in the UR account. UR generates the declaration from the user's KYC responses, and the user signs it as part of the KYC flow.

For most individual users, Form A confirms that the user is the beneficial owner of the assets. If another person is the beneficial owner, UR may need to collect that person's required identifying information. Depending on the partner UX, Form A can be handled inside the Sumsub process or integrated as a separate signing step. See [Component 4 in The User Lifecycle](/concepts/overview-the-user-lifecycle).

The declaration consolidates the user's confirmations into a single signed statement. It covers that the user is over 18, is not a national of North Korea, Iran, or Russia, and is not a US person; that the residential address and sole country of tax residence are accurate; that the user is the sole beneficial owner of the account assets; the declared source of funds; acceptance of the Terms of Use, Privacy Policy, and Card Terms; acceptance of the business risks of using UR; and the FINMA acknowledgment that deposits are not protected by any depositor-protection scheme. The statement also notes that deliberately providing false information is a criminal offense under article 251 of the Swiss Criminal Code. UR renders the exact text from the user's collected data and returns it for the user to read and sign; display it verbatim. For the fetch, sign, and submit endpoints, see the [Developer Resources](https://docs.ur.app/api-reference/account/managed-custody-mode).

### Tax self-certification (CRS)

Under the Common Reporting Standard (CRS), UR collects a tax-residency self-certification, either at onboarding or as an ongoing-KYC task. The user declares each country of tax residence as an ISO 3166-1 alpha-3 code, together with a Taxpayer Identification Number (TIN) whose format matches that country. A user with more than one tax residence provides one entry per residence. If a CRS task is due and the user misses its deadline, the account moves to `SoftBlocked` until the task is completed.

### AML & sanctions screening

UR runs automated AML (Anti-Money Laundering) screening throughout the user's lifecycle:

* **At onboarding:** the user is screened against international sanctions lists, PEP lists, and adverse media.
* **Continuously:** users are re-screened periodically to capture list changes after onboarding.
* **On transactions:** specific transactions may trigger additional risk-based checks, such as a step-up liveness check before high-value off-ramps. See `needLivenessCheck` in the [API reference](https://docs.ur.app/api-reference/account/managed-custody-mode).

A user who fails sanctions screening, is from an unsupported country, or has an unsupported nationality is rejected during compliance review. The user remains in `Tourist` status and cannot access core banking features.

## When KYC happens

UR runs KYC when a user opens a UR account and continues compliance monitoring throughout the user's lifecycle. After the initial KYC verification, UR may request additional information through ongoing KYC, client enhanced due diligence, or transaction enhanced due diligence depending on the user's profile, activity, and regulatory requirements.

| Review type                        | When it happens                                                                                            | What UR may collect                                                                                                                                                         |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Initial KYC verification           | During account opening, before the user can access core banking features                                   | Questionnaire, domicile verification, identity verification (NFC, penny transfer, or video), signed Beneficial Ownership Declaration (Form A), and AML/sanctions screening. |
| Ongoing KYC                        | Periodically, or when regulatory refresh rules require updated user information                            | Refreshed proof of address, updated questionnaire responses, refreshed identity documents, CRS self-certification, or a re-signed Form A.                                   |
| Client enhanced due diligence      | When the user's profile, country, occupation, source of funds, or other risk signals require deeper review | Additional information about account purpose, source of funds, source of wealth, expected activity, supporting documents, or compliance attestations.                       |
| Transaction enhanced due diligence | When a specific transaction or activity pattern triggers additional risk-based review                      | Transaction rationale, supporting documents, additional liveness checks, or other information needed to assess the transaction.                                             |

## Verification outcomes

KYC results map directly to the user's URID status:

| Outcome                                                        | URID status   | What the user can do                                                                       |
| -------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------ |
| URID minted, before KYC                                        | `Tourist`     | Receive partner off-ramp and inbound P2P (up to 250 USD/EUR); cannot move funds externally |
| KYC approved                                                   | `Live`        | All banking features: external transfers, IBAN, debit card, FX                             |
| Compliance review failed or user rejected                      | `Tourist`     | Core banking features unavailable                                                          |
| Pending ongoing KYC or enhanced due diligence, deadline missed | `SoftBlocked` | Money features locked until the open task is completed                                     |

For the on-chain status mapping (`Tourist=2`, `Live=5`, etc.), see [Smart Contracts](https://docs.ur.app/api-reference/smart-contracts).

The KYC step machine depends on the path. Partner-managed (FMA) partners who push data or hand over a share token see the coarse sequence `PartnerDataIngestion` -> `IdentityVerification` -> `SignFormA` -> `Register`, while FMA partners on the Sumsub SDK see the fine-grained Sumsub steps ending `IdOrPassportOrOtherIdInformationScan` -> `IdAndLiveness` -> `SignFormA` -> `Register`. The UR-hosted flow runs the same fine-grained steps. See the state tables in [Managed Custody SDK KYC](https://docs.ur.app/api-reference/kyc-and-kyb/managed-custody-sdk-kyc) and [Managed Custody Mode](https://docs.ur.app/api-reference/account/managed-custody-mode).

## Vendors

* **Sumsub:** KYC provider for the questionnaire, the domicile (device location) check, document upload, and AML/sanctions screening. When you integrate the Sumsub SDK directly (External Wallet Access Mode), the document scan, NFC chip read, and liveness check also run in the Sumsub mobile SDK.
* **ReadID:** in the UR-hosted webview path, performs the passport/national-ID NFC chip read, document scan, and selfie/liveness capture. The user opens ReadID as a mobile app or via the iOS App Clip.

UR sends the final compliance decision (`Pass` / `Rejected`) to you via the [`kyc_status` webhook](/developer-resources/webhook).

## Additional KYC after a user goes live

KYC does not end at onboarding. After a user becomes `Live`, UR may request additional verification when regulatory refresh rules, risk signals, or compliance reviews require updated information. Examples include a proof-of-address refresh, passport re-scan, re-signed Form A, full re-verification, or CRS self-certification. These requests map to the ongoing KYC and enhanced due diligence reviews described in [When KYC happens](#when-kyc-happens).

How you integrate additional KYC depends on which KYC model your `partnerId` uses. The two models do not share endpoints or webhooks, so read only the one that applies to you.

### Partners on the UR-hosted KYC flow

This covers External Wallet Access Mode and other integrations where the user completes KYC through UR's own flow. The task is discovered by polling, and the outcome arrives on `kyc_status`.

| Step                                 | What you do                                                                                                                                                                                                                                                                                                      | Where it's documented                                                                                                                                                                                                                                                                                                                                          |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **1. Detect the required task**      | Poll the account/status endpoints and read `kycRetryVerificationLevel`, `kycCurrentStep`, and `crsInfo` (`needCrs`, `restrictDate`, `url`) to see what the user must complete and by when.                                                                                                                       | [Fetch UR Account information](https://docs.ur.app/api-reference#fetch-ur-account-information), including the **KycRetryVerificationLevel** and **kycCurrentStep** enum tables; per-mode account status with `kycFlow` and `crsInfo` in the [External Wallet Access Mode API reference](https://docs.ur.app/api-reference/account/external-wallet-access-mode) |
| **2. Collect and submit the update** | Re-initiate the Sumsub flow for the required retry level using `isRetryVerification=true` and the matching `retryLevel`, with optional `stepType` when needed. The user then completes the requested step in the Sumsub SDK. For CRS, direct the user to the `crsInfo.url` link returned by the status endpoint. | Get Sumsub SDK Token (`/api/v1/sumsub/create-access-token`) in the [External Wallet Access Mode API reference](https://docs.ur.app/api-reference/account/external-wallet-access-mode)                                                                                                                                                                          |
| **3. Receive the decision**          | UR sends the compliance decision (`Pass` / `Rejected`) to your webhook. You can also query status again to confirm the latest result.                                                                                                                                                                            | [`kyc_status` webhook](/developer-resources/webhook); [Query Sumsub status by network](https://docs.ur.app/api-reference#query-sumsub-status-by-network)                                                                                                                                                                                                       |

The retry level in `kycRetryVerificationLevel`, such as `ResetAll`, `ResetSign`, `ResetGPS`, or `RetryVerificationPassport`, tells you which data points the user must provide again. See the enum tables in [OpenAPIs](https://docs.ur.app/api-reference#fetch-ur-account-information) for the full mapping.

{% hint style="warning" %}
The retry parameters above apply to `/api/v1/sumsub/create-access-token`. The by-network variant, [Create Sumsub access token by network](https://docs.ur.app/api-reference#create-sumsub-access-token-by-network), takes only `tokenId` and `network` and always issues a token at the flow's start level, so it cannot drive a specific retry level. If your users are on an external (non-Mantle) network, confirm the path for additional KYC through your dedicated integration channel.
{% endhint %}

If the user misses the deadline for an open task on this model, the user moves to `SoftBlocked` and money features stay locked until the task is completed.

### Partner-managed (FMA) partners

Partners who own the KYC user experience do not poll for these tasks. UR operations issue a retry directive, and your platform is notified over webhook. There is no polling step and no `kyc_status` event.

| Step                                        | What you do                                                                                                                                                                                                                                                         | Where it's documented                                                                                                              |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| **1. Receive the directive**                | Handle the `fma.additional_kyc.required` webhook. Its `taskType` says what the user must redo, and `dataChannel` says which of your integration paths applies. Neither your platform nor the user can start a retry.                                                | [Retry KYC](https://docs.ur.app/api-reference/kyc-and-kyb/retry-kyc); payload contract in [Webhooks](/developer-resources/webhook) |
| **2. Claim a session and collect the data** | Call `POST /api/fma/v1/kyc/session/create` to claim the session UR created, then run only the steps `taskType` names, through the same endpoint your channel uses for onboarding. Re-sign Form A, and call `/kyc/submit` only when `fiat24Mode` is `auto_register`. | [Retry KYC](https://docs.ur.app/api-reference/kyc-and-kyb/retry-kyc), including a per-channel walkthrough and lifecycle diagram    |
| **3. Record completion**                    | UR sends `fma.additional_kyc.completed` when the session reaches its last step. No further call is required.                                                                                                                                                        | [Retry KYC](https://docs.ur.app/api-reference/kyc-and-kyb/retry-kyc)                                                               |

On this model the directive's `deadlineAt` is informational: UR does not expire the directive or the session when it passes, and no event fires at the deadline. Compliance applies the consequences off-platform.


# Webhooks

How UR delivers business events to partner-registered HTTPS endpoints. Covers protocol, signatures, retries, idempotency, and the full event catalog.

This document is the canonical reference for partner backend engineers integrating with UR's webhook delivery service. It covers the wire protocol, signature verification, retry semantics, idempotency contract, and every event type currently in production.

If you only have a few minutes, read [§1 Overview](#1-overview) and [§4 Signature verification](#4-signature-verification). Everything else is reference material.

***

## 1. Overview

UR pushes asynchronous business events (transactions confirmed on-chain, KYC outcomes, allowance changes, card-authorization failures, partner-KYC session results) to an HTTPS endpoint you register in your partner contract.

The high-level contract:

| Aspect             | Behavior                                                                                         |
| ------------------ | ------------------------------------------------------------------------------------------------ |
| Transport          | `HTTPS POST`, `Content-Type: application/json`                                                   |
| Body               | JSON envelope `{event, data, timestamp}`                                                         |
| Authentication     | EIP-191 ECDSA signature in HTTP headers (V1 legacy + V2 recommended, both delivered in parallel) |
| Delivery semantics | At-least-once, \~48 h retry window, ±20 %-jittered exponential backoff                           |
| Success criterion  | Your endpoint returns HTTP 2xx within the configured timeout (default 30 s)                      |
| Idempotency        | We send `X-Webhook-Request-Id` (UUID v4); partners **MUST** dedupe on this                       |
| Ordering           | Sequential per-partner; across partners is unordered                                             |

> User-Agent: `URBank-Webhook/2.0` (pre-v2.0 partners saw `URBank-Webhook/1.0`). Whitelist by domain, not UA. The `URBank-Webhook` string is a fixed wire-protocol constant retained for backward compatibility; it is not a product name.

***

## 2. Endpoint requirements

Your endpoint **MUST**:

1. Be reachable via HTTPS with a CA-issued certificate (no self-signed).
2. Accept `POST` with `Content-Type: application/json`.
3. Return HTTP 2xx on success within the configured timeout (default 30 s).
4. Return HTTP 4xx on **permanent** failure; we will NOT retry.
5. Return HTTP 5xx (or time out) on **transient** failure; we WILL retry.
6. Be idempotent: handle the same `X-Webhook-Request-Id` more than once gracefully (every retry reuses the same id).

Your endpoint **SHOULD**:

* Verify at least one of the two signatures (see [§4](#4-signature-verification)) before processing.
* Reject any request whose `X-Webhook-Timestamp` skews more than ±5 minutes from your clock (replay protection).
* Ack fast (< 1 s ideal) and process asynchronously. A queue + worker pattern works best because our delivery worker holds a TCP connection open until you respond.

***

## 3. HTTP headers

Every outbound request carries these headers. `X-Webhook-*` are envelope metadata; `X-Api-*` are signature material.

| Header                 | Example              | Meaning                                                                                                        |
| ---------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------- |
| `Content-Type`         | `application/json`   | Always JSON                                                                                                    |
| `User-Agent`           | `URBank-Webhook/2.0` | Stable for the v2.0 generation of the delivery service                                                         |
| `X-Webhook-Request-Id` | `b3e8…-…` (UUID v4)  | **Idempotency key.** Same value across the first delivery and every retry of the same message. Dedupe on this. |
| `X-Webhook-Timestamp`  | `1735689600`         | Unix seconds at the moment the HTTP request is assembled                                                       |
| `X-Webhook-Event-Type` | `transaction`        | Canonical event name (see [§6 Event catalog](#6-event-catalog))                                                |
| `X-Webhook-Attempt`    | `1`, `2`, …          | **1-indexed delivery attempt.** First delivery is `1`, first retry is `2`.                                     |
| `X-Api-Signature`      | `0x…`                | **V1 (legacy) signature.** Present whenever V1 is enabled in your contract.                                    |
| `X-Api-PublicKey`      | `0x…`                | V1 signer Ethereum address. Same address as V2.                                                                |
| `X-Api-Signature-V2`   | `0x…`                | **V2 (recommended) signature.** Present whenever V2 is enabled.                                                |
| `X-Api-PublicKey-V2`   | `0x…`                | V2 signer Ethereum address (= V1 address).                                                                     |

### 3.1 Response headers you can send

| Header                     | When                                                                              | Effect                                                       |
| -------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| `X-Webhook-No-Retry: true` | On a 4xx or 5xx response when you've decided this message should never be retried | We move the message to `DEAD` immediately and stop retrying. |

Everything else in your response is ignored. Status code is what we key on (see [§5](#5-retry-policy)).

***

## 4. Signature verification

All webhooks are signed with the **EIP-191** "personal sign" scheme used by Ethereum wallets:

```
prefix  = "\x19Ethereum Signed Message:\n" + len(message)
hash    = Keccak256(prefix + message)
sig     = ECDSA-secp256k1(server_private_key, hash)   // 65 bytes, 0x-prefixed hex
recover = ecrecover(hash, sig)                        // → server address
```

The signing **key** is the same for V1 and V2; only the signed **message** differs.

### 4.1 V1 (legacy)

```
message = body                                         // raw JSON bytes, byte-for-byte
header  = X-Api-Signature: 0x{65-byte hex}
```

V1 is what legacy partners have always verified. It will remain on the wire indefinitely; a retirement announcement will run on a ≥3-month deprecation window if we ever do retire it.

### 4.2 V2 (recommended)

```
message = timestamp + "." + request_id + "." + body
        = "1735689600.b3e8ffff-0000-4000-8000-000000000000.{...json...}"
header  = X-Api-Signature-V2: 0x{65-byte hex}
```

V2 binds the timestamp and request id into the signature. Without that binding an attacker who captures a webhook can replay the body with a fresh timestamp; with V2 the recovered address won't match. **All new integrations should validate V2.**

### 4.3 Address pinning

The signer address is published in your partner contract. Pin it on your side; do **not** trust whatever `X-Api-PublicKey(-V2)` says without comparing to the pinned value. The header is a convenience; the contract is the source of truth.

### 4.4 Reference implementation (Node.js)

The verifier call moved namespace between ethers major versions. Both forms are shown below; the rest of the function is identical.

**ethers v6** (current; published 2023+):

```js
const { verifyMessage } = require("ethers");

function verifyV2(req, expectedSigner) {
  const ts   = req.headers["x-webhook-timestamp"];
  const id   = req.headers["x-webhook-request-id"];
  const body = req.rawBody.toString("utf8");          // raw, before JSON.parse
  const sig  = req.headers["x-api-signature-v2"];

  const message = `${ts}.${id}.${body}`;
  const recovered = verifyMessage(message, sig);      // v6: top-level export
  return recovered.toLowerCase() === expectedSigner.toLowerCase();
}
```

**ethers v5** (legacy; for partners on the v5 line):

```js
const { ethers } = require("ethers");

function verifyV2(req, expectedSigner) {
  // ... ts / id / body / sig / message identical to v6 above ...
  const recovered = ethers.utils.verifyMessage(message, sig);   // v5: under .utils
  return recovered.toLowerCase() === expectedSigner.toLowerCase();
}
```

If you're not sure which version you're on, run `node -p "require('ethers').version"`. `6.x.x` is v6; `5.x.x` is v5. The hash + secp256k1 recovery underneath is the same EIP-191 algorithm in both versions, so the recovered address is byte-identical.

### 4.5 Reference implementation (Go)

```go
import (
    "fmt"
    "strings"
    "github.com/ethereum/go-ethereum/common"
    "github.com/ethereum/go-ethereum/crypto"
)

func verifyV2(ts, reqID, body, sigHex, expectedAddr string) (bool, error) {
    msg      := fmt.Sprintf("%s.%s.%s", ts, reqID, body)
    prefixed := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(msg), msg)
    hash     := crypto.Keccak256Hash([]byte(prefixed))

    sig := common.FromHex(sigHex)
    if sig[64] >= 27 { sig[64] -= 27 }
    pub, err := crypto.SigToPub(hash.Bytes(), sig)
    if err != nil {
        return false, err
    }
    return strings.EqualFold(crypto.PubkeyToAddress(*pub).Hex(), expectedAddr), nil
}
```

### 4.6 Verifying both versions during the migration window

A partner who verifies V2 only and rejects on failure will break the day UR rotates a key (rare but planned). Defense-in-depth:

```
if  X-Api-Signature-V2 present:  verify V2 → 200 if OK
elif X-Api-Signature   present:  verify V1 → 200 if OK
else:                            401 missing signature
```

A common partner-side bug is parsing JSON first and then re-serializing for signature verification. That breaks because Go's `encoding/json` does **not** preserve key order. **Read the raw request body byte-for-byte before deserializing.** The `req.rawBody` / `c.Request.GetBody()` pattern in most frameworks does this for you.

***

## 5. Retry policy

### 5.1 Classification by response

| Outcome                                         | Class                                                            |
| ----------------------------------------------- | ---------------------------------------------------------------- |
| HTTP 2xx                                        | Success (message marked `DELIVERED`).                            |
| HTTP 4xx (except 408, 429)                      | Non-retryable (message marked `DEAD`).                           |
| HTTP 408, 429                                   | Retryable (server-side hint that you were rate-limited or slow). |
| HTTP 5xx                                        | Retryable.                                                       |
| Network error / timeout / DNS failure           | Retryable.                                                       |
| HTTP response header `X-Webhook-No-Retry: true` | Treated as non-retryable regardless of status.                   |

### 5.2 Backoff schedule

±20 % jittered exponential backoff, base 30 s, cap 6 h, max **20 attempts** over roughly a **48-hour** window:

| Attempt | Approx. delay since previous |
| ------- | ---------------------------- |
| 1       | (initial)                    |
| 2       | \~30 s                       |
| 3       | \~1 min                      |
| 4       | \~2 min                      |
| 5       | \~4 min                      |
| …       | doubling each step, jittered |
| 11      | \~4 h                        |
| 12      | \~6 h (cap)                  |
| 13 … 20 | \~6 h each                   |

After attempt 20 the message is moved to `DEAD` and persisted for 90 days. UR operations can manually re-queue a `DEAD` message on partner request.

### 5.3 Circuit breaker

If we observe 20 consecutive failures (5xx / timeout / connection-refused) for a given partner, a per-partner circuit opens for 60 s. Open-circuit attempts are not sent to your endpoint; they're requeued for the next backoff slot. Your retry count is **not** incremented while the circuit is open.

### 5.4 What `X-Webhook-Attempt` tells you

The header is 1-indexed:

* `Attempt: 1`: first delivery (fresh message)
* `Attempt: 2+`: retry

You can use this to bias logging (verbose on `1`, terse on retries) or to detect partner-side hot loops where retries keep failing for the same root cause.

***

## 6. Event catalog

Subscription is **per-partner, per-event-type**. You only receive event types your contract subscribes to. Asking for an event you're not subscribed to is the most common silent-fail in integrations; if you don't see events you expect, check your subscription before debugging anything else.

New event types may be added; existing event-type bodies are **append-only**. We may add new optional fields, never rename or remove existing ones.

### 6.0 Field-naming quirks (read this first)

The customer identifier appears under one of two field names depending on event type, and its JSON type also varies between events. These are **wire-fixed** across all delivered events; partners must handle both shapes.

| Event types                                                                                                                                                  | Field name | JSON type                 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- | ------------------------- |
| `transaction`, `transaction_v2`, `card_spending_failed`                                                                                                      | `urId`     | string                    |
| `allowance`                                                                                                                                                  | `urId`     | int64 (number, no quotes) |
| `fma.account.result`, `fma.penny_drop.result`, `fma.kyc.reuse_check.result`, `fma.kyc.result`, `fma.additional_kyc.required`, `fma.additional_kyc.completed` | `urId`     | int64 (number, no quotes) |
| `kyc_status`, `sumsub_kyc_result`, `monthly_limit_exceeded`                                                                                                  | `tokenId`  | string                    |

All four shapes refer to the **same** UR NFT token id. The split is legacy: the `tokenId` family predates the unification on `urId`, and the int64/string split predates the unification on string. The wire shapes are frozen for backwards compatibility; do not assume future events will pick one canonical form.

Other quirks worth flagging up-front:

* **Subscription changes** (enable / disable an event type, rotate webhook URL) are coordinated with your UR technical liaison; there is no self-service API.
* **Token symbols** referenced in event bodies (`USD24`, `EUR24`, `CHF24`, `JPY24`, `CNH24`, `SGD24`, `HKD24`, `USDC`) are the UR-issued tokenized fiat assets; see [Tokenized deposits](/concepts/tokenized-deposits) for the catalogue and contract addresses.
* **Wire-fixed JSON keys** such as `deadLine` (capital `L`, in `card_spending_failed`) are preserved verbatim from legacy schemas. Where they appear, the field reference table for that event notes the quirk explicitly.

### 6.1 Money movement & on-chain events

#### `transaction` (legacy transaction event)

Emitted when a tracked transaction is confirmed on-chain (deposit, withdrawal, transfer, FX, card spend).

```json
{
  "event": "transaction",
  "timestamp": 1735689600,
  "data": {
    "urId": "1000001234",
    "title": "Sent USD24",
    "subtitle": "to 0xAbC…",
    "amount": "100.00",
    "type": "transfer_out",
    "timestamp": 1735689599,
    "currency": "USD24",
    "direction": "out",
    "txHash": "0xdead…beef",
    "chainId": "eip155:5000",
    "inputToken": "USD24",
    "inputAmount": "100.00",
    "inputTokenAddress": "0x…"
  }
}
```

Bidirectional fan-out: for a P2P transfer between two of your customers (both bound to your `partner_id`) you receive **two** webhooks: one with `direction=out` and the sender's `urId`, and one with `direction=in` and the receiver's `urId`. For a self-transfer (same `urId` on both sides) business-key dedup collapses to a single webhook.

Field reference (see also [TransactionData in the API reference](https://docs.ur.app/api-reference/account/delegated-contract-mode#transactiondata)):

| Field                                                          | Type                | Description                                                            |
| -------------------------------------------------------------- | ------------------- | ---------------------------------------------------------------------- |
| `urId`                                                         | string              | Token ID of the customer this event belongs to                         |
| `title`, `subtitle`, `image`                                   | string              | UI strings; partner can ignore or display                              |
| `amount`, `currency`                                           | string              | Display amount (decimal string) and ticker; partner-visible            |
| `type`                                                         | string              | Business type (e.g. `transfer_in`, `transfer_out`, `card_spend`, `fx`) |
| `direction`                                                    | string              | `in` or `out` from this customer's perspective                         |
| `txHash`, `chainId`                                            | string              | On-chain identifiers; `chainId` is CAIP-2 (e.g. `eip155:5000`)         |
| `inputToken`, `inputAmount`, `inputTokenAddress`               | string              | Source asset                                                           |
| `outputAmount`                                                 | string (omitempty)  | FX leg                                                                 |
| `mcc`                                                          | uint64 (omitempty)  | Merchant Category Code (card spend)                                    |
| `reference`, `bankAccount`, `crdMultiToken`, `partnerRefId`, … | various (omitempty) | Channel-specific extras                                                |

#### `transaction_v2` (current transaction event)

Same business event as `transaction`, but a normalized schema and lifecycle (`broadcastTimeE9`, `finalTimeE9`). New partners should subscribe to `transaction_v2` and ignore `transaction`.

```json
{
  "event": "transaction_v2",
  "timestamp": 1735689600,
  "data": {
    "id": 42,
    "txHash": "0xdead…beef",
    "txLogIndex": 0,
    "blockNumber": 12345678,
    "createTimeE9": 1735689500000000000,
    "broadcastTimeE9": 1735689510000000000,
    "finalTimeE9": 1735689600000000000,
    "type": "transfer_out",
    "chainId": "eip155:5000",
    "chainName": "Mantle",
    "urId": "1000001234",
    "direction": "out",
    "amount": "100.00",
    "currency": "USD24",
    "status": "CONFIRMED",
    "detailsJson": "{…}",
    "reqId": "…",
    "refundType": ""
  }
}
```

| Field        | Type   | Description                                                                                                                                                                                                                                                                                                                               |
| ------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `refundType` | string | Refund discriminator. `""` (not a refund), `BANK_REFUND` (bank transfer refund), `CARD_REFUND` (card spending refund), or `CARD_REVERSAL` (card authorization reversal). See [§12.4 Identifying refund transactions](https://docs.ur.app/api-reference/account/managed-custody-mode#id-12.4-identifying-refund-transactions) for details. |

`detailsJson` is a stringified JSON blob whose structure depends on `data.type`. See [detailsJson reference by transaction type](https://docs.ur.app/api-reference/account/managed-custody-mode#id-12.3-detailsjson-reference-by-transaction-type) for the full schema of each type.

#### `allowance` (ERC-20 allowance change)

Emitted when one of your customer's allowance flags flips (approve / revoke / transferFrom drains it).

```json
{
  "event": "allowance",
  "timestamp": 1735689600,
  "data": {
    "urId": 1000001234,
    "allowances": [
      { "tokenSymbol": "USD24", "hasAllowance": true,  "chainId": "eip155:5000" },
      { "tokenSymbol": "EUR24", "hasAllowance": false, "chainId": "eip155:5000" }
    ]
  }
}
```

| Field               | Type   | Description                                                                  |
| ------------------- | ------ | ---------------------------------------------------------------------------- |
| `data.urId`         | int64  | Customer UR id (note: int64 here, not string; legacy quirk)                  |
| `data.allowances[]` | array  | Updated allowance rows                                                       |
| `…tokenSymbol`      | string | One of `USD24`, `EUR24`, `CHF24`, `JPY24`, `CNH24`, `SGD24`, `HKD24`, `USDC` |
| `…hasAllowance`     | bool   | True iff on-chain allowance > 0 for the UR contracts                         |
| `…chainId`          | string | CAIP-2 chain id                                                              |

Only the **rows that changed** are present; never a full snapshot. Treat `allowances` as a patch, not a state dump.

#### `card_spending_failed` (card authorization rejected for user-caused reason)

Emitted when a customer's card spend fails because of insufficient balance / missing allowance / similar customer-actionable reason. Not for network or processor failures.

```json
{
  "event": "card_spending_failed",
  "timestamp": 1735689600,
  "data": {
    "urId": "1000001234",
    "status": "FAILED",
    "reason": "INSUFFICIENT_ALLOWANCE",
    "transactionCurrency": "EUR",
    "transactionAmount": "42.00",
    "cardCurrency": "EUR",
    "deadLine": "2025-12-31T23:59:59Z",
    "authorizationToken": "…",
    "txHash": "0x…",
    "transactionTime": "2025-12-30T10:15:30Z"
  }
}
```

| Field                                      | Type               | Description                                                                                                                                                                                                 |
| ------------------------------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `urId`                                     | string             | Customer UR id                                                                                                                                                                                              |
| `status`                                   | string             | Always `FAILED` for this event                                                                                                                                                                              |
| `reason`                                   | string (omitempty) | Machine-readable reason code                                                                                                                                                                                |
| `transactionCurrency`, `transactionAmount` | string (omitempty) | Original authorization details                                                                                                                                                                              |
| `cardCurrency`                             | string (omitempty) | Card's billing currency                                                                                                                                                                                     |
| `deadLine`                                 | string (omitempty) | Customer-action deadline (ISO 8601). **Wire-fixed**: the JSON key is literally `deadLine` (capital `L`), not the more conventional `deadline`. Preserved verbatim for backwards-compat and will not change. |
| `authorizationToken`                       | string (omitempty) | Reference for the rejected authorization                                                                                                                                                                    |

#### `monthly_limit_exceeded` (monthly spending limit hit)

Emitted when a customer's monthly spend in CHF would cross their configured limit.

```json
{
  "event": "monthly_limit_exceeded",
  "timestamp": 1735689600,
  "data": {
    "tokenId": "1000001234",
    "txAmount": "150.00",
    "txCurrency": "EUR",
    "availableCHF": "0.00",
    "usedCHF": "10000.00",
    "maxCHF": "10000.00"
  }
}
```

| Field                               | Type   | Description                           |
| ----------------------------------- | ------ | ------------------------------------- |
| `tokenId`                           | string | Customer UR id                        |
| `txAmount`, `txCurrency`            | string | Transaction that triggered the breach |
| `availableCHF`, `usedCHF`, `maxCHF` | string | Limit accounting in CHF               |

### 6.2 KYC (generic)

#### `kyc_status` (KYC status change)

Emitted when UR returns an updated compliance status for a customer.

```json
{
  "event": "kyc_status",
  "timestamp": 1735689600,
  "data": {
    "tokenId": "1000001234",
    "status": "Pass"
  }
}
```

| Field     | Type   | Description                                                    |
| --------- | ------ | -------------------------------------------------------------- |
| `tokenId` | string | Customer UR id                                                 |
| `status`  | string | One of: `Pending`, `Pass`, `Rejected`, `ManualReview`, `Error` |

#### `sumsub_kyc_result` (Sumsub review result)

Emitted when a Sumsub KYC review completes for one of your customers.

```json
{
  "event": "sumsub_kyc_result",
  "timestamp": 1735689600,
  "data": {
    "tokenId": "1000001234",
    "kycResult": "Pass"
  }
}
```

| Field       | Type   | Description          |
| ----------- | ------ | -------------------- |
| `tokenId`   | string | Customer UR id       |
| `kycResult` | string | `Pass` or `Rejected` |

### 6.3 Partner-managed (FMA) KYC

These events fire only for partners using the **Fiat-Mediated Account (FMA)** integration model: partners who own the KYC user-experience and let UR run downstream compliance. They are **not** sent to non-FMA partners.

The FMA events share a common envelope shape (per-event exceptions are noted in each entry):

* `urId` (int64): UR token id (issued by UR-side mint after `create-account`)
* `sessionId` (string, UUID): the partner-KYC session this event belongs to (present on `fma.account.result`, `fma.penny_drop.result`, `fma.kyc.reuse_check.result`, `fma.additional_kyc.completed`; **absent** on `fma.kyc.result` and on `fma.additional_kyc.required`, which fires before the retry session exists)
* `partnerId` (string): your partner id (same string you sign requests with)
* `status` (string): a state discriminator inside one logical event family (the `fma.additional_kyc.*` pair does not carry it: the event name itself is the discriminator)
* `occurredAt` (int64): Unix seconds at emission (the `fma.additional_kyc.*` pair uses `createdAt` and `completedAt` instead)

#### `fma.account.result` (account onboarding outcome)

Single event that subsumes "account activated" and "session rejected" outcomes. Branch on `status`:

| `status`    | Meaning                                                                                                 | Extra fields                 |
| ----------- | ------------------------------------------------------------------------------------------------------- | ---------------------------- |
| `activated` | KYC passed end-to-end. Customer is now Live on-chain with their CHF spending limit set.                 | (none)                       |
| `rejected`  | The session ended in failure (compliance hard-rejected, KYC-rejected, draft expired, or retry expired). | `rejectCode`, `rejectReason` |

```json
{
  "event": "fma.account.result",
  "timestamp": 1735689600,
  "data": {
    "urId": 1000001234,
    "sessionId": "b3e8ffff-0000-4000-8000-000000000000",
    "partnerId": "partner_example",
    "status": "activated",
    "occurredAt": 1735689600
  }
}
```

```json
{
  "event": "fma.account.result",
  "timestamp": 1735689700,
  "data": {
    "urId": 1000001234,
    "sessionId": "b3e8ffff-0000-4000-8000-000000000000",
    "partnerId": "partner_example",
    "status": "rejected",
    "rejectCode": "KYC_REJECTED",
    "rejectReason": "KYC status=REJECTED (checkCount=3)",
    "occurredAt": 1735689700
  }
}
```

`rejectCode` enumeration (the values you will see in production, grouped by what caused the rejection):

| `rejectCode`               | Meaning                                                                                                                                    | Origin          |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------- |
| `KYC_REJECTED`             | Compliance review returned a hard reject. Partner should NOT retry.                                                                        | KYC pipeline    |
| `UPSTREAM_RETRY_EXHAUSTED` | We could not reach an upstream (Sumsub or the compliance backend, etc.) within the 48-hour retry budget. Partner may request ops re-queue. | KYC pipeline    |
| `INTERNAL_ERROR`           | Defensive catch-all for a bug on our side. Always paired with an incident on UR's side; contact ops.                                       | KYC pipeline    |
| `SUMSUB_REJECTED`          | Sumsub returned a hard reject during identity verification.                                                                                | KYC pipeline    |
| `FORMA_NOT_SIGNED`         | Customer never signed Form A within the session lifetime.                                                                                  | KYC pipeline    |
| `SESSION_EXPIRED`          | Draft session expired (never made it past the data-ingestion step within its TTL).                                                         | Maintenance job |
| `PENNY_TIMEOUT`            | Session reached `IdentityVerification` but neither penny-drop nor NFC completed within 30 days.                                            | Maintenance job |
| `STALE_ORPHAN_GC`          | Garbage-collection of orphaned legacy session rows; should not appear for newly-onboarded partners.                                        | Maintenance job |

Additional `Reject*` codes from the API-level error catalogue may appear if a partner-side API call triggers a session termination (rare). Treat any unknown code as "session ended; surface `rejectReason` to your support tooling and ask UR ops for translation". The set above covers >99 % of real production rejections.

When you receive `status=activated` you can trust:

* The customer's UR NFT is **Live** on-chain
* Their `setClientLimit` transaction has confirmed
* It is safe to allow card spends / withdrawals / etc.

#### `fma.penny_drop.result` (penny-drop verification)

Per-attempt verification result for the penny-drop flow (applies to partners whose KYC variant requires a micro-transfer match step; subscription is per partner). Branch on `status`:

| `status`   | Meaning                                                                                                                                                                                                     |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `verified` | The customer's micro-transfer matched our expected amount; KYC advances to `IdentityVerification → SignFormA`.                                                                                              |
| `failed`   | The most recent attempt did not match. The customer can retry up to a configured cap. When the last allowed attempt fails, the payload includes `exhausted: true` and no further attempts will be accepted. |

```json
{
  "event": "fma.penny_drop.result",
  "timestamp": 1735689600,
  "data": {
    "urId": 1000001234,
    "sessionId": "b3e8ffff-…",
    "partnerId": "partner_example",
    "status": "verified",
    "amount": { "value": "1.23", "currency": "EUR" },
    "remitterCountryISO3": "CHE",
    "transactionRecordId": 987654,
    "txHash": "0xabc…def",
    "txLogIndex": 0,
    "occurredAt": 1735689600
  }
}
```

```json
{
  "event": "fma.penny_drop.result",
  "timestamp": 1735689700,
  "data": {
    "urId": 1000001234,
    "sessionId": "b3e8ffff-…",
    "partnerId": "partner_example",
    "status": "failed",
    "amount": { "value": "0.50", "currency": "EUR" },
    "remitterCountryISO3": "CHE",
    "failReason": "AMOUNT_BELOW_MIN",
    "attemptsRemaining": 0,
    "exhausted": true,
    "transactionRecordId": 987655,
    "txHash": "0xabc…f01",
    "txLogIndex": 0,
    "occurredAt": 1735689700
  }
}
```

Field reference (every field below is present on every delivery of this event type unless marked optional):

| Field                 | Type                                       | Present when         | Description                                                                                                                                                                                                                                                                                                      |
| --------------------- | ------------------------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `urId`                | int64                                      | always               | UR token id                                                                                                                                                                                                                                                                                                      |
| `sessionId`           | string (UUID)                              | always               | Partner-KYC session id                                                                                                                                                                                                                                                                                           |
| `partnerId`           | string                                     | always               | Your partner id                                                                                                                                                                                                                                                                                                  |
| `status`              | string                                     | always               | `verified` or `failed`                                                                                                                                                                                                                                                                                           |
| `amount`              | object `{value: string, currency: string}` | always               | The micro-transfer amount we observed (decimal string + 3-letter ISO currency)                                                                                                                                                                                                                                   |
| `remitterCountryISO3` | string                                     | always               | The remitter person's country, ISO 3166-1 alpha-3. Used by partner UI for compliance display.                                                                                                                                                                                                                    |
| `failReason`          | string                                     | `status=failed` only | Machine-readable reason. Common values: `AMOUNT_MISMATCH`, `AMOUNT_BELOW_MIN`, `CURRENCY_MISMATCH`, `IBAN_MISMATCH`. Treat unknown values as "failed; ask UR ops for translation".                                                                                                                               |
| `attemptsRemaining`   | int                                        | `status=failed` only | How many additional attempts the customer has after this one. `0` means no more attempts will be accepted by `/kyc/penny-transfer`.                                                                                                                                                                              |
| `exhausted`           | bool                                       | `status=failed` only | `true` iff `attemptsRemaining=0`. When `true`, the customer must switch to the NFC fallback path; the penny-drop endpoint will return `PENNY_DROP_MAX_ATTEMPTS_EXCEEDED` for any further calls. The session itself is **not** terminated; an `fma.account.result` may still follow after the NFC path completes. |
| `transactionRecordId` | int64                                      | always               | `ur_transaction_records.id` of the evaluated on-chain fiat-deposit mint. Stable identity for the deposit this result refers to.                                                                                                                                                                                  |
| `txHash`              | string                                     | always               | On-chain tx hash of the evaluated fiat-deposit mint.                                                                                                                                                                                                                                                             |
| `txLogIndex`          | int64                                      | optional             | Event ordinal inside the tx; disambiguates when a single tx hash carries multiple mint events. Omitted when not applicable.                                                                                                                                                                                      |
| `occurredAt`          | int64                                      | always               | Unix seconds at emission                                                                                                                                                                                                                                                                                         |

#### `fma.kyc.reuse_check.result` (shared-token handoff verdict)

Fires only for partners using the **shared-token (Copy Applicant) KYC reuse** model. It is the async mirror of a handoff attempt's conclusive verdict: emitted once per conclusive attempt for `status ∈ {passed, incomplete}`. **Terminal (non-remediable) verdicts are deliberately NOT webhooked**; the partner learns those from the synchronous handoff response / `POST /api/fma/v1/kyc/check` and UR ops. A lost delivery degrades to polling `/kyc/check`, so this event is fire-and-forget on our side.

```json
{
  "event": "fma.kyc.reuse_check.result",
  "timestamp": 1735689600,
  "data": {
    "urId": 1000001234,
    "partnerId": "partner_example",
    "sessionId": "b3e8ffff-0000-4000-8000-000000000000",
    "attempt": 1,
    "status": "incomplete",
    "correlationId": "…",
    "missingFields": ["identity.document", "liveness"],
    "requiredLevels": ["id-and-liveness"],
    "occurredAt": 1735689600
  }
}
```

| Field            | Type          | Present when             | Description                                                                                                     |
| ---------------- | ------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `urId`           | int64         | always                   | UR token id                                                                                                     |
| `partnerId`      | string        | always                   | Your partner id                                                                                                 |
| `sessionId`      | string (UUID) | always                   | The handoff session this verdict belongs to                                                                     |
| `attempt`        | int           | always                   | Session-scoped monotonic attempt counter (part of the business key, so redeliveries of the same attempt dedupe) |
| `status`         | string        | always                   | `passed` (import accepted) or `incomplete` (remediable; resubmit the missing data and re-handoff)               |
| `correlationId`  | string        | always                   | Trace id for cross-referencing with UR ops                                                                      |
| `missingFields`  | string\[]     | `status=incomplete` only | Dot-path identifiers of the data still required                                                                 |
| `requiredLevels` | string\[]     | optional                 | Cloned-workflow level names mapped from `missingFields`, when resolvable                                        |
| `occurredAt`     | int64         | always                   | Unix seconds at emission                                                                                        |

#### `fma.kyc.result` (Sumsub review outcome for shared-token flows)

Fires for shared-token / pull / SDK FMA flows when the underlying Sumsub review reaches an outcome. This is the **Sumsub-layer KYC verdict**, distinct from `fma.account.result` (which is the account-*activation* outcome). Branch on `status`.

| `status`   | Meaning                                                                              |
| ---------- | ------------------------------------------------------------------------------------ |
| `pass`     | Identity data accepted; the flow has advanced past all Sumsub levels to `SignFormA`. |
| `rejected` | Sumsub returned a hard reject (`RED`).                                               |
| `step_up`  | Additional documents requested. (Reserved; not emitted by this path yet.)            |

```json
{
  "event": "fma.kyc.result",
  "timestamp": 1735689600,
  "data": {
    "urId": 1000001234,
    "partnerId": "partner_example",
    "status": "rejected",
    "rejectCode": "SUMSUB_REJECTED",
    "rejectReason": "…",
    "occurredAt": 1735689600
  }
}
```

| Field          | Type   | Present when           | Description                               |
| -------------- | ------ | ---------------------- | ----------------------------------------- |
| `urId`         | int64  | always                 | UR token id                               |
| `partnerId`    | string | always                 | Your partner id                           |
| `status`       | string | always                 | `pass`, `rejected`, or `step_up`          |
| `rejectCode`   | string | `status=rejected` only | Machine-readable reject reason            |
| `rejectReason` | string | optional               | Free-form reason text for support tooling |
| `occurredAt`   | int64  | always                 | Unix seconds at emission                  |

> Note: `fma.kyc.result` carries no `sessionId`; correlate on `urId` + `occurredAt`.

#### `fma.additional_kyc.required` (ops triggered a partner-driven retry)

Emitted when UR operations decide an already-onboarded customer must resubmit part or all of their KYC. UR seals the customer's current session as it emits this event, so any `sessionId` you still hold stops working. The new session is **not** in the payload: you create it by calling `POST /api/fma/v1/kyc/session/create`, which returns its id.

```json
{
  "event": "fma.additional_kyc.required",
  "timestamp": 1735689600,
  "data": {
    "directiveId": "62fb1d29-e584-448f-a770-9454c94dbe24",
    "type": "retry",
    "taskType": "passport",
    "retryLevel": 6,
    "fiat24Mode": "ops_offline",
    "dataChannel": "sdk",
    "partnerId": "partner_example",
    "externalUserId": "partner-side-user-id-xyz",
    "urId": 1000001234,
    "retryOfSessionId": "b3e8ffff-0000-4000-8000-000000000000",
    "retryReason": "Compliance review: document expired",
    "requiredFields": [],
    "deadlineAt": 0,
    "createdAt": 1735689600
  }
}
```

Field reference:

| Field              | Type          | Description                                                                                                                                                                                |
| ------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `directiveId`      | string (UUID) | Identifies this retry request. Correlates with the `completed` event and with UR support tickets.                                                                                          |
| `type`             | string        | Always `retry` today; other values are reserved.                                                                                                                                           |
| `taskType`         | string        | **What the customer must redo**, and the field to branch on: `full`, `passport`, `address_recheck`, or `redo_form_a`.                                                                      |
| `retryLevel`       | int           | The retry category ops selected. Advisory; `taskType` is authoritative. Note this is a different quantity from the `retryLevel` returned by `/kyc/session/current`, which counts sessions. |
| `fiat24Mode`       | string        | `auto_register` or `ops_offline`. Decides whether you finish the session with `POST /kyc/submit`.                                                                                          |
| `dataChannel`      | string        | `push`, `sdk`, or `shared-token`. Matches your integration path.                                                                                                                           |
| `partnerId`        | string        | Your partner id                                                                                                                                                                            |
| `externalUserId`   | string        | The partner-side user id you bound to this `urId` at `create-account` time                                                                                                                 |
| `urId`             | int64         | UR token id (unchanged between the original and retry sessions)                                                                                                                            |
| `retryOfSessionId` | string (UUID) | The session this retry replaces. It is already sealed; calls against it return `SESSION_NOT_FOUND` (`30006`).                                                                              |
| `retryReason`      | string        | Free-form reason text suitable for partner-side support tooling (not for direct end-user display)                                                                                          |
| `requiredFields`   | string\[]     | Push channel only: dot-path identifiers to correct, for example `registerRequest.address.street`. Empty on other channels.                                                                 |
| `deadlineAt`       | int64         | Unix seconds, or `0` when unset. Informational only: UR does not expire the directive or session when it passes, and no event fires at the deadline.                                       |
| `createdAt`        | int64         | Unix seconds when the retry was triggered                                                                                                                                                  |

Action on receipt: call `POST /api/fma/v1/kyc/session/create` to claim the new session, then drive the customer through the steps `taskType` names. See [Retry KYC](https://docs.ur.app/api-reference/kyc-and-kyb/retry-kyc) for the per-channel walkthrough.

#### `fma.additional_kyc.completed` (retry session finished)

Emitted when the retry session reaches its last step. No further call is required from you.

```json
{
  "event": "fma.additional_kyc.completed",
  "timestamp": 1735692800,
  "data": {
    "directiveId": "62fb1d29-e584-448f-a770-9454c94dbe24",
    "type": "retry",
    "taskType": "passport",
    "retryLevel": 6,
    "fiat24Mode": "ops_offline",
    "dataChannel": "sdk",
    "partnerId": "partner_example",
    "externalUserId": "partner-side-user-id-xyz",
    "urId": 1000001234,
    "sessionId": "c0ffee00-0000-4000-8000-000000000000",
    "retryOfSessionId": "b3e8ffff-0000-4000-8000-000000000000",
    "completedAt": 1735692800
  }
}
```

`directiveId`, `taskType`, `retryLevel`, and `fiat24Mode` repeat the values from the `required` event, so the pair matches without a lookup. Two fields differ:

| Field         | Type          | Description                                                                              |
| ------------- | ------------- | ---------------------------------------------------------------------------------------- |
| `sessionId`   | string (UUID) | The retry session that just completed, the same id `/kyc/session/create` returned to you |
| `completedAt` | int64         | Unix seconds when the session completed                                                  |

> Subscribe to **both** `fma.additional_kyc.required` and `fma.additional_kyc.completed`. An unsubscribed event is dropped, so a partner subscribed to only one of the pair silently sees half the flow.

***

## 7. Idempotency and ordering

### 7.1 What to dedupe on

**Dedupe on `X-Webhook-Request-Id`.** That UUID is fixed for the lifetime of a message: every retry sends the same id, and our database uniqueness constraint on `(event_type, partner_id, business_key)` guarantees the same logical event never gets two request ids.

Concretely on your side:

```
on_receive(req):
  if already_processed(req.headers["X-Webhook-Request-Id"]):
    return 200 OK
  verify_signature(req); if !ok: return 401
  process(req.body)
  mark_processed(req.headers["X-Webhook-Request-Id"], ttl=7d)
  return 200 OK
```

### 7.2 What NOT to dedupe on

* The HTTP body, or any subset of fields inside it. Two different business events (e.g. status `activated` and a later status `rejected` for the same session id after a retry flow) can share field values, and you do want to receive both.
* `X-Webhook-Timestamp`: that changes on every retry.

### 7.3 Ordering guarantees

* **Per-partner first-delivery order**: the **first delivery attempts** for messages belonging to one partner go out in the same order they were produced. The internal Kafka topic is partitioned by `partner_id` and a single consumer goroutine per partition processes strictly sequentially, advancing the Kafka offset as soon as each delivery attempt completes (whether success or failure).
* **Retries are out-of-band**: a failed message moves to `PENDING_RETRY` and is re-published to the same partition by a separate retry dispatcher after its backoff. The consumer **does not block on retries**; it proceeds to the next fresh message immediately. As a consequence, **a retry of message A may arrive after a later message B** if B's first delivery happens to succeed while A is still cycling. Build your handler so that re-applying message A after message B's effects is safe.
* **Across partners**: no ordering guarantee.
* **Within an FMA session**: the `fma.account.result` event for a `sessionId` is the **final word** for that session; once you've seen it, no further events for the same `sessionId` will arrive. Earlier `fma.penny_drop.result` events for the same `sessionId` are produced first, but the retry caveat above still applies (a delayed retry could theoretically arrive after `account.result`). If you order on the partner side by `occurredAt`, you'll get the intended chronology even on a delayed retry.

***

## 8. Operations

### 8.1 Rate limits

Per-partner default: 20 requests per second, burst 40. If you can sustain higher and need it, raise it with your UR technical liaison.

### 8.2 IP allowlists

Our outbound IP is stable per Kubernetes deployment but may shift on node rotation. **Allowlist by domain, not by IP.** If you must allowlist by IP, ask for the current allocation in writing and treat it as advisory; we won't notify before rotations.

### 8.3 TLS

Standard CA chain. We don't pin certificates on our side; you don't need to share anything for handshake.

### 8.4 Recovery probing

During incident response our system may re-drive delivery to your endpoint, but it does **not** send a synthetic probe event; there is no `__probe__` event type. Recovery reuses the actual business messages (replayed from the internal Kafka topic), so anything you receive is a real, dedupable event carrying its normal `X-Webhook-Request-Id`. Keep handling every delivery idempotently (see §7); there is no special probe body to special-case.

### 8.5 Cancellation

UR operations can cancel a `PENDING_RETRY` message via internal admin tooling (for example, if a refund / reversal makes an earlier event obsolete). Messages already in flight may still arrive at your endpoint for a few seconds after cancellation. **Treat late arrivals idempotently**; your business-side reconciliation should already handle this.

***

## 9. Troubleshooting

| Symptom                                                   | Likely cause                                                                                                   | Fix                                                                                            |
| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Signature verification fails on every webhook             | You're re-serializing the body after JSON-parse                                                                | Read `req.rawBody` (or framework equivalent); sign over raw bytes, not re-encoded              |
| Some webhooks verify, others don't                        | Two different keys delivered (V1 vs V2 with different message composition); you're using one verifier for both | Implement both verifiers (see [§4.6](#46-verifying-both-versions-during-the-migration-window)) |
| Webhooks for one customer suddenly stop                   | Your endpoint is returning non-2xx and we hit the 20-attempt cap → `DEAD`                                      | Check your logs at `X-Webhook-Request-Id`; ask UR ops to re-queue                              |
| Webhooks for ALL customers stop briefly, then resume      | Circuit breaker opened on our side after 20 consecutive failures from your endpoint                            | Look at the 60 s before resumption for the failure mode                                        |
| Same `X-Webhook-Request-Id` arrives 5× in a minute        | Your endpoint is timing out (we keep retrying) or returning 5xx                                                | Acknowledge fast; process asynchronously                                                       |
| Webhook count is roughly half what you expected           | Subscription not enabled for the missing event type                                                            | Check your partner contract subscriptions                                                      |
| FMA `activated` arrives but customer is not Live on-chain | Should never happen; `activated` is only fired **after** the on-chain status flip and limit-set both confirm   | Capture the `X-Webhook-Request-Id` and `sessionId`, contact UR ops                             |

For unresolved incidents, open a ticket via your UR technical liaison with at minimum:

* `X-Webhook-Request-Id`
* `X-Webhook-Timestamp`
* `X-Webhook-Event-Type`
* The full request body (raw bytes)
* The HTTP status you returned and the response body

***

## 10. Quick reference card

```
POST {your endpoint}
Host: {your domain}
Content-Type: application/json
User-Agent: URBank-Webhook/2.0
X-Webhook-Request-Id:  b3e8ffff-0000-4000-8000-000000000000
X-Webhook-Timestamp:   1735689600
X-Webhook-Event-Type:  transaction_v2
X-Webhook-Attempt:     1
X-Api-Signature:       0x…                                  (V1, optional in V2-only mode)
X-Api-PublicKey:       0xServerAddress
X-Api-Signature-V2:    0x…                                  (V2, recommended)
X-Api-PublicKey-V2:    0xServerAddress

{"event":"transaction_v2","data":{…},"timestamp":1735689600}
```

Verification message for V2:

```
"1735689600.b3e8ffff-0000-4000-8000-000000000000.{"event":"transaction_v2","data":{…},"timestamp":1735689600}"
```

Return `200 OK` to ack. Return `4xx` to fail permanently. Return `5xx` / time out to retry.

***

## See also

* [Signature and verify](https://docs.ur.app/api-reference/signature-and-verify): generic UR request-signing rules and the signature scheme for the **partner → UR** direction. Note: that doc currently documents only the V1 webhook signature (UR → partner direction); the V2 scheme defined in [§4.2](#42-v2-recommended) of this page is the authoritative webhook reference. The EIP-191 primitive is shared across both directions, but the message composition differs (request-body + deadline for partner → UR; timestamp + request-id + body for V2 webhooks).
* [OpenAPIs](https://docs.ur.app/api-reference): full reference for the partner REST API; `TransactionData` and friends are documented there.
* [Partner integration flow concepts](/welcome/partner-integration-flow-concepts): when each event fires in the customer lifecycle.
* [The user lifecycle](/concepts/overview-the-user-lifecycle): end-to-end customer journey, with webhook touchpoints.


# Build with AI coding agents

Use AI agents to query UR documentation and build integrations faster.

Use AI in your UR integration workflow. We provide tools that give AI agents direct access to our documentation, so you can ask questions, generate code, and build integrations without copy-pasting docs into prompts.

## Install the UR Docs MCP server

Our [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server gives any AI coding assistant direct access to UR documentation. Three tools are available to any connected client:

| Tool          | What it does                         |
| ------------- | ------------------------------------ |
| `search_docs` | Search docs by keyword               |
| `get_page`    | Get full page content in markdown    |
| `list_pages`  | Browse all pages and their hierarchy |

### Claude Code

Run in your terminal:

```bash
claude mcp add ur-docs --transport http https://ur-docs-mcp-production.up.railway.app/mcp
```

### Claude Desktop

Add to your config file (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):

```json
{
  "mcpServers": {
    "ur-docs": {
      "url": "https://ur-docs-mcp-production.up.railway.app/mcp"
    }
  }
}
```

### Cursor

Open **Settings > MCP Servers > Add new server**:

* Name: `ur-docs`
* Type: `HTTP`
* URL: `https://ur-docs-mcp-production.up.railway.app/mcp`

### VS Code (Copilot)

Add to `.vscode/mcp.json` in your project:

```json
{
  "servers": {
    "ur-docs": {
      "url": "https://ur-docs-mcp-production.up.railway.app/mcp"
    }
  }
}
```

### Windsurf

Add to `~/.codeium/windsurf/mcp_config.json`:

```json
{
  "mcpServers": {
    "ur-docs": {
      "serverUrl": "https://ur-docs-mcp-production.up.railway.app/mcp"
    }
  }
}
```

## Plain text docs

You can access our documentation as plain text at [docs.ur.app/llms-full.txt](https://docs.ur.app/llms-full.txt). This helps AI tools consume our content directly and allows you to paste full documentation context into an LLM.

Plain text is preferable to scraping HTML because:

* Fewer formatting tokens
* LLMs parse and understand markdown hierarchy
* Content hidden in tabs or collapsed sections is included

## Try it

After setting up the MCP server, restart your AI client and try:

* "What is UR and how does the account layer work?"
* "How do I integrate with UR in Managed Custody Mode?"
* "What's the user lifecycle from KYC to card spending?"
* "Search UR docs for tokenized deposits"

Your assistant will search and read our docs to give you an accurate, up-to-date answer.


# Design for UR partners

Design and UX guidance for partners building on UR.

This section helps you present UR's account inside your product and design your co-branded card. It is written for designers and frontend developers.

UR provides a real financial account, not just a wallet: the account layer your users live financially on. The multi-currency card complements the account; it is not the primary product. These guidelines help you position UR correctly, structure your account screens, and design the flows your users will follow.

### User experience

How to position UR's account and card, structure your account screens, and design the account creation, KYC, card, and money movement flows. Start with [Introduction](/design/user-experience/introduction) for what UR provides and what it is not.

### Cards

The artwork specification for your physical and virtual card. See [Co-branded debit card design](/design/cards/co-branded-debit-card-design) to download the template, customize your card, and submit it for approval.


# Introduction

What UR provides, and what it is not.

This guide helps you integrate UR's account in a way that accurately represents the product and creates the best experience for your users.

UR provides a real financial account: a Swiss IBAN account that holds and moves stablecoins and fiat as one, not just a wallet. The multi-currency card is a valuable spending tool that complements the account; it is not the primary product. Following these guidelines helps your users understand the full value of the account and sets the right expectations from the start.

### What UR provides

UR offers a real financial account that lets your users:

* Hold and manage multi-currency balances
* Receive funds via incoming transfer to their Swiss IBAN
* Send funds to external accounts
* Convert digital assets to fiat (off-ramp)
* Convert fiat to digital assets (on-ramp)
* Spend their balance using a multi-currency card

The account is the foundation. The card is a convenient way to access and spend the funds held in that account.

### What UR is not

UR is not primarily a card product or a payment card service. The multi-currency card is a valuable feature, but position it as one of several ways your users can use their account, not as the main offering.


# Messaging hierarchy

What to lead with, what to support with, and what to avoid.

Order your messaging so the account leads and the card supports. This section lists what to emphasize first, what to introduce next, and what to avoid.

### Primary messaging

Lead with the account. Put the total account balance front and center, break it down by currency, and surface the actions that make the account useful: deposit, send, and convert.

### Secondary messaging

Once the account value is established, introduce spending. This is where the multi-currency debit card, card management, and transaction history belong; they round out the account rather than define it.

### What to avoid

Do not position UR primarily as a card. Framing such as "get your crypto card", a payment card service, a crypto card offering, or any card-first value proposition buries the account that funds it.


# Copy guidelines

Recommended terminology for account opening and feature descriptions.

Use account-first language everywhere your users read about UR. The tables below show the wording to use and the wording to avoid.

### Account opening and setup

| Use this              | Not this           |
| --------------------- | ------------------ |
| "Open your account"   | "Apply for a card" |
| "Get started with UR" | "Card application" |

### Feature descriptions

| Use this                                    | Not this           |
| ------------------------------------------- | ------------------ |
| "Manage your balance"                       | "Top up your card" |
| "Deposit funds to your account"             | "Card balance"     |
| "Send money to any external account"        | "Load your card"   |
| "Convert crypto directly into your account" |                    |
| "Spend your balance with a debit card"      |                    |


# Partner positioning of UR

How to introduce UR in banners and provide persistent account access.

Position UR as an embedded financial capability inside your product, not as an external service. This page covers how to introduce UR for acquisition and how to keep the account accessible afterward.

### Banner and marketing placement for introduction and acquisition

Use your introduction surfaces to acquire users into the account, in your brand's voice.

{% columns %}
{% column %}
Lead with the benefit to the user, whether that is faster payouts, better rates, or more control, and mention "real account with a Swiss IBAN" early for credibility. Position UR as an integrated part of your product rather than an external app, and write in your brand's voice, not UR's. Avoid "get the UR card" or "get the partner card" messaging, which sells the card instead of the account.
{% endcolumn %}

{% column %}

<figure><img src="/files/U83Gb0hrzM4zrCHbzs4M" alt=""><figcaption><p>Banner example</p></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

### Fixed entry points for UR account access

Beyond the introduction banner, provide persistent access to UR account features through a fixed entry point on your home screen. A fixed entry point lets your users reach their UR account to check balances and transactions without searching through menus.

{% columns %}
{% column %}
**User benefits:**

* Instant access to account balance
* Quick actions without navigation
* Less friction for frequent actions
  {% endcolumn %}

{% column %}
**Partner benefits:**

* Higher feature engagement
* Fewer support queries ("where's my account?")
* Better user retention
* A clear value proposition that stays visible
  {% endcolumn %}
  {% endcolumns %}

{% columns %}
{% column %}

<figure><img src="/files/CtH9Wgm7wlbWs6hX6sFS" alt=""><figcaption><p>Example of a fixed entry point</p></figcaption></figure>
{% endcolumn %}

{% column %}

<figure><img src="/files/dmSKglpUzCtpS6GQu7g8" alt=""><figcaption><p>Another example of a fixed entry point</p></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

{% hint style="info" icon="figma" %}
Figma reference:\
[**Fixed entry point**](https://www.figma.com/design/WKbdwKpCGPRaAN43tGXHiN/Partner-UX-Guide?node-id=1-3\&t=HrYv0jeJvqeSoQlT-1)
{% endhint %}


# Visual hierarchy

How much screen prominence to give the account, actions, and card.

Give the account the most visual weight, core account actions the next, and card features the least. Use the prominence split below as a starting point for your account screens.

{% columns %}
{% column %}

* **Account information (60% prominence)**
  * Account balance (primary visual element)
  * Currency balances
  * Recent account activity
* **Core account actions (30% prominence)**
  * Deposit funds
  * Send funds
  * Convert
* **Card features (10% prominence)**
  * Card management
    {% endcolumn %}

{% column %}

<figure><img src="/files/HEywOnXeRwuQXC9HcBr2" alt=""><figcaption><p>Account page example</p></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

{% hint style="info" icon="figma" %}
Figma reference:\
[**Account dashboard**](https://www.figma.com/design/WKbdwKpCGPRaAN43tGXHiN/Partner-UX-Guide?node-id=1-5\&t=LdSer2mHyoMOtsCs-1)
{% endhint %}


# Account creation

The authentication paths for creating a UR account, and the URID.

Your users must establish their UR identity and link it to their account on your platform before they can access account features. This process does two things: account creation mints the [on-chain credential](https://docs.ur.app/api-reference/smart-contracts#account) that represents the UR account and Know Your Customer (KYC) identity, and account linking connects your platform identity to that UR account.

### Two authentication paths

Offer one of two ways to authenticate during account creation.

#### Google Sign-In (recommended)

Google Sign-In is the fastest path. It leans on Google's own authentication and security, so your users skip the CAPTCHA and one-time password (OTP) and never fumble with a verification code.

```mermaid
flowchart LR
    A[Continue with Google] --> B[Account created on UR]
```

#### Email authentication

Email authentication works for anyone, with no Google account needed. It takes a few more steps, where you might need to build a CAPTCHA to prevent bots and then an OTP screen to prove email ownership. In exchange, you get more control by not relying on Google Sign-In.

```mermaid
flowchart LR
    A[Continue with email] --> B[CAPTCHA to prevent bots]
    B --> C[Email OTP verification]
    C --> D[Account created on UR]
```

{% hint style="info" icon="figma" %}
Figma reference:\
[**Account creation**](https://www.figma.com/design/WKbdwKpCGPRaAN43tGXHiN/Partner-UX-Guide?node-id=55-6395\&t=4YF7ojkFUmbkiA7z-1)
{% endhint %}

### URID

The URID is the unique identifier generated when a user creates a UR account. It works like an account number, but it is the `tokenId` of the user's Account NFT on-chain, so anyone can verify it. This makes it the primary identifier for customer support and internal operations: support agents look users up by URID, and it cross-references partner systems against UR infrastructure. Because every URID is unique across all partners, it stays stable no matter which platform the user came through.

#### Technical background

Each UR user is represented on-chain by an Account NFT, and the URID is the `tokenId` of that NFT. That makes it the canonical on-chain identity for the user, not merely a database key: it is a verifiable, immutable identifier rooted in the blockchain.

To understand how Account NFTs are structured and managed, see the [Account section in Smart contracts](https://docs.ur.app/api-reference/smart-contracts#account).

#### Recommended placement

{% columns %}
{% column %}
Your users rarely need the URID in normal operations, but it must stay accessible for support scenarios.

Give it low visual priority so it does not compete with high-priority information or actions.
{% endcolumn %}

{% column valign="bottom" %}

<figure><img src="/files/QIupvvvkwoDKLgkBVW3q" alt="" width="375"><figcaption><p>Example of low-priority but always accessible placement of the URID</p></figcaption></figure>
{% endcolumn %}
{% endcolumns %}


# KYC

The three-part KYC flow and how to show the URID throughout it.

Know Your Customer (KYC) verification lets your users activate their account and unlock full account functionality. The flow has three sequential parts that verify identity, collect required information, and meet regulatory requirements.

For the full account and KYC lifecycle, see [The user lifecycle](https://docs.ur.app/concepts/overview-the-user-lifecycle) in the main UR docs.

### Three-part KYC structure (5 to 10 minutes)

{% stepper %}
{% step %}
**Questionnaire**

Tool: Sumsub\
Purpose: collect user information and assess risk profile\
Duration: 2 to 3 minutes
{% endstep %}

{% step %}
**Biometric ID verification**

Tool: ReadID App Clip (iOS) or ReadID app (Android)\
Purpose: verify document authenticity via NFC chip scanning and a liveness check\
Duration: 2 to 3 minutes
{% endstep %}

{% step %}
**Form A signing**

Purpose: regulatory compliance and consent documentation\
Duration: 1 to 2 minutes
{% endstep %}
{% endstepper %}

### Design principles

#### Transparent expectations

Tell your users what is required before they start: "3 steps: answer questions, scan ID, sign form." Setting expectations reduces anxiety and abandonment.

#### Platform-appropriate tools

Different tools for iOS (App Clip) and Android (full app) optimize for each platform's capabilities while keeping functional parity.

#### Security through familiarity

Established third-party tools (Sumsub, ReadID) build on the trust your users already place in recognized verification providers.

#### Auto-save progress

If a user exits mid-questionnaire, they can resume without restarting.

### Pre-KYC entry point

#### Managed Custody Mode

After a successful UR account creation, your users see the KYC prompt. If they dismiss the prompt or stop midway, they can continue KYC from an account dashboard or equivalent surface on your site.

{% columns %}
{% column %}

<figure><img src="/files/9xCEOdz5tVes9E0MSNv9" alt=""><figcaption><p>After successful account creation and connection, users see the KYC prompt.</p></figcaption></figure>
{% endcolumn %}

{% column %}

<figure><img src="/files/qZMlz48oTXux2T14fF8z" alt=""><figcaption><p>Account dashboard: a card showing "Verify your identity" in the pre-KYC state.</p></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

{% hint style="info" icon="figma" %}
Figma reference:\
[**KYC**](https://www.figma.com/design/WKbdwKpCGPRaAN43tGXHiN/Partner-UX-Guide?node-id=61-7302\&t=LdSer2mHyoMOtsCs-1)
{% endhint %}

### Displaying the URID during KYC

The URID is the user's support identifier. During custom partner KYC flows, your users should always be able to read and share this value when they need support.

#### Where the URID comes from

Retrieve the URID by way of `tokenId` from the Account NFT contract by calling `tokenOfOwnerByIndex(userAddress, 0)`.

{% content-ref url="<https://docs.ur.app/api-reference/smart-contracts#account>" %}
<https://docs.ur.app/api-reference/smart-contracts#account>
{% endcontent-ref %}

#### When to display it

Keep the URID on screen for the full duration of KYC: through every step of onboarding, on each retry or failure screen, and until the user either completes KYC or exits the flow.

#### UX recommendations

Keep the URID always visible in a fixed area such as a top bar or sticky footer, with a one-tap way to copy it. Its visual priority should stay low so it does not compete with the primary KYC actions.


# Card activation

Currency authorization and card activation by integration mode.

Card activation bridges account verification and spending capability. This page covers the currency authorization users complete during activation and the authentication patterns that protect card details, for both Managed Custody Mode and External Wallet Access Mode.

### Currency authorization and enablement

For regulatory reasons, your users must authorize currencies for payment. To prevent payments from failing at the point of purchase, include currency authorization in the card activation flow.

{% hint style="success" %}
Enable all currencies by default to maximize payment success. For wallet partners, currencies must be enabled via EIP-7702 to batch the transactions.
{% endhint %}

### Managed Custody Mode

<img src="/files/anmPMfjoQ615ZbduE4i7" alt="" class="gitbook-drawing">

{% hint style="info" icon="figma" %}
Figma reference:\
[**Managed Custody Mode card activation**](https://www.figma.com/design/WKbdwKpCGPRaAN43tGXHiN/Partner-UX-Guide?node-id=28-107\&t=LdSer2mHyoMOtsCs-1)
{% endhint %}

**Authentication requirement**

Every "View card" action requires re-authentication, for both activated and inactive cards. Card details are highly sensitive information, comparable to physical card access. Gating them protects against unauthorized access: someone with temporary access to an unlocked phone still cannot read the full card number and CVV. It also follows the industry standard set by financial apps, and it builds trust by tying card access to a verification step users recognize. Authentication adds friction, but the trade-off is acceptable because users expect this measure for financial data.

In Managed Custody Mode, users authenticate with their UR credentials (Google Sign-In or email one-time password (OTP)) to view card details, and they activate through your standard authentication flow via UR's webview. Currency authorization is included within the activation flow.

### External Wallet Access Mode

<img src="/files/7sPQ49gHqGmecrDuNMTl" alt="" class="gitbook-drawing">

{% hint style="info" icon="figma" %}
Figma reference:\
[**External Wallet Access Mode card activation**](https://www.figma.com/design/WKbdwKpCGPRaAN43tGXHiN/Partner-UX-Guide?node-id=135-6297\&t=LdSer2mHyoMOtsCs-1)
{% endhint %}

**Authentication requirement**

Every "View card" action requires wallet authentication, for both activated and inactive cards. The same reasoning applies as in Managed Custody Mode: it protects against unauthorized access, follows industry standards, and creates a security ritual that builds trust.

In External Wallet Access Mode, users authenticate with their wallet (for example, biometric or PIN) to view card details. The wallet signature proves ownership and intent. To activate the card, users sign with their wallet, and currency authorization is recommended as part of the activation flow.

Viewing sensitive card data (full number, CVV) requires cryptographic proof of wallet ownership every time.


# Card management

The card hub for security and spending controls.

The card page is the central hub for card security and spending preferences.

{% columns %}
{% column %}
From the card page, your users view their card details, manage currency authorization, monitor spending limits, and control the card's security features.
{% endcolumn %}

{% column %}

<figure><img src="/files/xobJzXBQKZNsjz6URAu9" alt=""><figcaption><p>Card management example</p></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

{% hint style="info" icon="figma" %}
Figma reference:\
[**Card management**](https://www.figma.com/design/WKbdwKpCGPRaAN43tGXHiN/Partner-UX-Guide?node-id=83-4148\&t=HrYv0jeJvqeSoQlT-1)
{% endhint %}


# Off-ramp

Converting digital assets to fiat in the UR account.

UR's off-ramp service lets your users convert digital assets into fiat and deposit the funds directly into their UR account. It connects on-chain value with the regulated financial system, so users can cash out digital assets at competitive rates with transparent fees.

### How it works

Users start an off-ramp transaction from your platform, choosing the digital asset to convert, the amount to cash out, and the destination currency. The converted fiat appears in the UR account balance within minutes, ready to spend with the Mastercard debit card.

{% hint style="info" icon="figma" %}
Figma reference:\
[**Off-ramp**](https://www.figma.com/design/WKbdwKpCGPRaAN43tGXHiN/Partner-UX-Guide?node-id=29-4447\&t=LdSer2mHyoMOtsCs-1)
{% endhint %}


# Bank transfer

Receiving funds from external bank accounts into the UR account.

UR's bank transfer service lets your users receive funds from external bank accounts held in their own name into their UR account. Users can accept incoming transfers in CHF, EUR, and USD from their personal bank accounts at other financial institutions, which gives them funding options beyond off-ramping digital assets.

### How it works

For security, users authenticate before they can access their IBAN details. They provide these details to their external bank to start a transfer. Funds arrive in the UR account within standard banking timeframes.

{% hint style="info" icon="figma" %}
Figma reference:\
[**Bank transfer**](https://www.figma.com/design/WKbdwKpCGPRaAN43tGXHiN/Partner-UX-Guide?node-id=31-6328\&t=LdSer2mHyoMOtsCs-1)
{% endhint %}


# Co-branded debit card design

Design and submit your custom co-branded Mastercard debit card artwork.

A co-branded card puts your brand on a real UR virtual Mastercard, so your users see something that looks and feels like yours while UR handles issuing and settlement. If you plan to offer cards to your users, follow these steps to design the artwork, license your brand to UR, and submit the card for approval.

The card is virtual. Your users add it to Apple Pay, Google Pay, or a similar wallet and spend it from their phone; there is no physical card to print or ship. The artwork you create here is the face they see in those wallets and inside your app. Approval and compliance review take time, so start early and plan for it ahead of any launch date.

{% stepper %}
{% step %}
**Download and customize the template**

Start from the official Adobe Illustrator template. It is set up with the correct dimensions and safe areas, so your artwork lines up with how the card renders in the app.

{% file src="/files/ABg4DuX5ZQmA7UtJ0Zwo" %}

Choose either the dark or light color scheme as your base, then customize the card surface color to match your brand. Keep every design element inside the dashed safe area; anything outside it can be clipped or interfere with the card's functional zones.
{% endstep %}

{% step %}
**Export your design correctly**

Export a single flattened image that meets the format and compliance requirements. Getting this right avoids a rejection later in approval.

Your final card design must be:

* PNG format
* 1536 × 969 pixels
* The debit mark, included

Do not include any of the following, which either do not apply to a virtual card or are prohibited on Mastercard artwork:

* EMV chip faceplate or contacts
* Magnetic stripe
* Embossed elements
* Transparency overlays
* Holograms
* Rounded corners
* Shading or 3D effects
  {% endstep %}

{% step %}
**Authorize use of your brand**

UR needs your permission to display your brand on a Mastercard product. Provide the Copyright Registration documentation for your logo, then complete and sign the Trademark License Agreement below. This grants UR the limited right to reproduce your mark on the card.

{% file src="/files/fhmWVT0o9otGqxIqzU6W" %}
{% endstep %}

{% step %}
**Submit your design to UR for approval**

Send your exported artwork and signed agreement to UR through your dedicated integration channel. UR reviews the design against Mastercard brand standards and the card's functional constraints. Approval takes time, so submit early; UR will tell you if anything needs to change before the card can be issued.
{% endstep %}
{% endstepper %}


# OpenAPIs

Base URLs, environments, and authentication for all UR API endpoints.

## API configurations

### Base URLs

The API is deployed across different environments. Use the appropriate base URL for your integration stage.

| Environment    | Base URL                          |
| -------------- | --------------------------------- |
| **Testnet**    | `https://uropenapi-qa.ur-inc.xyz` |
| **Production** | `https://openapi.ur.app`          |

### API authentication

The authentication method for the following Core Banking APIs refers to [this document](/api-reference/signature-and-verify#part-a-partner-authentication-ur-open-api-and-webhooks).

## Mint URID

**Endpoint: POST /v1/mint/nft**

Allows the Partner to create a registered user in the UR system via an API call and simultaneously mint a URID NFT for the user.

Before calling this endpoint, first query the Account NFT contract with `tokenOfOwnerByIndex` to check whether the user address already owns an NFT:

* If an NFT already exists, reuse the existing `tokenId` and do not call mint again
* If no NFT exists, call this endpoint to mint a new URID

Calling mint for an address that already has an NFT can return `30001` (the EVM address already has an NFT).

**Request Parameters**:

```json
{
  "email": "example@test.com",
  "evmAddress": "0xD4FcD82E3589b81A6de532e92E574761CC619531",
  "signature": "0x7f561411c39e993c807cd49d336e32bc2ec5e9ca12035a7efaa724ca760f97dc66f45a73b1b5dbf605127ee9ec5989e88972ba6c9a09d3684b0dec436229cc2701",
  "hash": "test-hash-1768792402",
  "deadline": "1768792702"
}
```

| Field        | Type   | Required | Description                                                                                                                                             |
| ------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `email`      | string | Yes      | User's email address                                                                                                                                    |
| `evmAddress` | string | Yes      | User's EVM Wallet Address                                                                                                                               |
| `signature`  | string | Yes      | User's wallet signature (EIP-191). See [User authentication](/api-reference/signature-and-verify#part-b-user-authentication-ur-api) for signing details |
| `hash`       | string | Yes      | A Keccak256 hash of the business payload or a unique message                                                                                            |
| `deadline`   | string | Yes      | Unix timestamp (seconds) for signature expiration                                                                                                       |

**User Signature Generation**:

* Construct base message: `baseMessage = hash + deadline` (string concatenation)
* Generate intermediate hash: `intermediateHash = Keccak256(baseMessage)`
* Construct final message: `finalMessage = "I agree to access my profile. " + intermediateHash.hex()`
* User signs the `finalMessage` using their wallet (EIP-191)

For detailed signing and verification rules, please refer to [Signature and Verification](/api-reference/signature-and-verify).

**Response Example**:

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "tokenId": 6021634448,
    "txHash": "0xb21c3e15de2f85fad9b04729f6db9ed6477fe08524b4db91a8e35ef2ab08039e"
  }
}
```

**Response Field Description**:

* `tokenId`: The minted NFT Token ID, required for all subsequent API calls
* `txHash`: On-chain transaction hash

## Fetch UR Account information

**Endpoint: POST /v1/profile**

Get user profile information including on-chain status and ERC20 authorization information.

**Request Parameters**:

```json
{
  "urId": 12345,
  "authCode": "UAC_a1b2c3"
}
```

| Field      | Type   | Required | Description                                                                                                |
| ---------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------- |
| `urId`     | int64  | No       | UR user ID, can directly query on-chain information                                                        |
| `authCode` | string | No       | OAuth authorization code for future extension (can exchange via authorization code when `urId` is missing) |

> **Note**: At least one of `urId` or `authCode` must be provided. Currently, the endpoint primarily uses `urId` for queries, `authCode` field is for future authorization code extension scenarios.

**Response Example**:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "urId": 12345,
    "chainStatus": 5,
    "evmAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
    "usedLimit": 120000000,
    "clientLimit": 500000000,
    "startLimitDate": 1714377600,
    "allowances": [
      {
        "tokenSymbol": "USD",
        "hasAllowance": true
      },
      {
        "tokenSymbol": "EUR",
        "hasAllowance": true
      },
      {
        "tokenSymbol": "CHF",
        "hasAllowance": false
      },
      {
        "tokenSymbol": "JPY",
        "hasAllowance": true
      },
      {
        "tokenSymbol": "CNH",
        "hasAllowance": false
      },
      {
        "tokenSymbol": "SGD",
        "hasAllowance": false
      },
      {
        "tokenSymbol": "HKD",
        "hasAllowance": false
      }
    ],
    "kycRetryVerificationLevel": 0,
    "kycRetryVerificationLevelStr": "UNKNOWN",
    "kycCurrentStep": 0,
    "kycCurrentStepStr": "UNKNOWN",
    "bankAccounts": {
      "CHF": [
        {
          "account": "CH93 0000 0000 0001 2345 6",
          "bankName": "SR Saphirstein AG",
          "bankAddress": "Bellerivestrasse 245, 8008 Zurich, Switzerland",
          "bic": "SAHHCHZ2"
        }
      ],
      "EUR": [
        {
          "account": "CH93 0000 0000 0001 2345 6",
          "bankName": "SR Saphirstein AG",
          "bankAddress": "Bellerivestrasse 245, 8008 Zurich, Switzerland",
          "bic": "SAHHCHZ2"
        }
      ]
    },
    "crsInfo": {
      "needCrs": true,
      "restrictDate": 1778402365685,
      "url": "https://ur-fe2.qa4.gomantle.org/info-provide/crs?hash=D9cEvsYk&source=ur"
    }
  }
}
```

**Response Field Description**:

* `urId`: UR NFT ID
* `chainStatus`: [On-chain status](https://docs.ur.app/concepts/overview-the-user-lifecycle#user-states)
  * `1` - SoftBlocked
  * `2` - Tourist
  * `3` - Blocked
  * `4` - Closed
  * `5` - Live (Normal)
* `evmAddress`: User's EVM address
* `usedLimit`: Current Accumulated Usage: The total volume used within the current 30-day billing cycle. This value is retrieved directly from the UR [smart contract ](/api-reference/smart-contracts#account)on-chain.
  * <mark style="color:$info;">The value is denominated in CHF with 2 decimal places (e.g., a return value of</mark> <mark style="color:$info;">`505`</mark> <mark style="color:$info;">represents</mark> <mark style="color:$info;">`5.05 CHF`</mark><mark style="color:$info;">). This limit is incremented by all fiat-related operations, including Card Spending, FX, On-ramping, and Cash Payouts.</mark>
* `clientLimit`: Maximum Monthly Limit: The total spending capacity allocated to the user for a 30-day period.
  * <mark style="color:$info;">The unit and retrieval method are identical to</mark> <mark style="color:$info;">`usedLimit`</mark><mark style="color:$info;">.</mark> <mark style="color:$warning;">Any transaction (FX, Card Spending, On-ramp, or Cash Payout) that exceeds this threshold will be rejected by the smart contract. Partners are highly recommended to perform a pre-transaction check against the user's remaining limit (</mark><mark style="color:$warning;">`clientLimit`</mark> <mark style="color:$warning;">-</mark> <mark style="color:$warning;">`usedLimit`</mark><mark style="color:$warning;">) before initiating payment flows.</mark>
* `startLimitDate`: Limit start timestamp (seconds, UTC)
* `allowances`: ERC20 authorization list (batch queried on-chain via multicall)
  * `tokenSymbol`: Token symbol (USD, EUR, CHF, JPY, CNH, SGD, HKD)
  * `hasAllowance`: true if allowance has been set, otherwise false
* `kycRetryVerificationLevel`: KYC retry verification level (integer enum)
* `kycRetryVerificationLevelStr`: KYC retry verification level string
* `kycCurrentStep`: KYC current step (integer enum)
* `kycCurrentStepStr`: KYC current step string
* `bankAccounts`: Bank account information, keyed by currency code (e.g., `CHF`, `EUR`). Each currency maps to an array of bank account objects:
  * `account`: IBAN account number (generated based on user's NFT ID, Swiss IBAN format)
  * `bankName`: Bank name (e.g., `SR Saphirstein AG`)
  * `bankAddress`: Bank address
  * `bic`: SWIFT/BIC code (e.g., `SAHHCHZ2`)
* `crsInfo`: CRS information :
  * `needCrs`: Indicates whether the user is required to complete the CRS process.
  * `restrictDate`: Represents the deadline or restriction timestamp for completing the CRS process, usually in Unix timestamp format (milliseconds). After this time, the user may be softBlocked.
  * `url`: The CRS submission link that directs the user to the CRS information collection page.

\*\*

**KycRetryVerificationLevel Enum**:

| Value | Name                      | Description                                                       |
| ----- | ------------------------- | ----------------------------------------------------------------- |
| `0`   | UNKNOWN                   | Default value, Normal First KYC                                   |
| `1`   | ResetAll                  | Clear Sumsub + Sign, redo KYC + signature                         |
| `2`   | ResetSumsub               | Clear Sumsub KYC info, restart KYC                                |
| `3`   | ResetSign                 | Clear signature info, re-sign                                     |
| `4`   | ResetGPS                  | Proactive GPS reset (created manually in admin)                   |
| `5`   | RetryGPS                  | Passive GPS retry (auto-created when GPS fails) **(Deprecated)**  |
| `6`   | RetryVerificationPassport | Supplement passport info                                          |
| `7`   | ResetKYCToReadID          | Reset KYC flow, use Sumsub + ReadID to redo KYC **(Deprecated)**  |
| `8`   | TransactionEdd            | Transaction-related EDD questionnaire (created manually in admin) |

**kycCurrentStep Enum**:

| Value | Name      | Description   |
| ----- | --------- | ------------- |
| `0`   | UNKNOWN   | Default value |
| `1`   | FormA     | Form A step   |
| `2`   | IDScan    | ID Scan step  |
| `3`   | SignFormA | Sign Form A   |
| `4`   | Review    | Review step   |
| `5`   | Rejected  | Rejected      |

## Fetch UR Account balance

**Endpoint: POST /v1/balance**

Get user balance information.

**Request Parameters**:

```json
{
  "urId": 12345
}
```

| Field  | Type  | Required | Description |
| ------ | ----- | -------- | ----------- |
| `urId` | int64 | Yes      | UR user ID  |

**Response Example**:

```json
{
  "code": 0,
  "message": "",
  "data": [
    {
      "symbol": "USDC",
      "amount": "1000.50"
    },
    {
      "symbol": "USD24",
      "amount": "5000.00"
    }
  ]
}
```

**Response Field Description**:

* `symbol`: Token symbol
* `amount`: Balance amount, formatted according to Token's `Decimals` precision (e.g., 2 or 6 decimal places)

## Fetch transaction history

**POST /v1/transactions**

Get user transaction records, supports multi-condition filtering and cursor pagination. The endpoint returns top-level pagination markers (`hasNextPage`, etc.) and structured information for each transaction.

**Request Parameters**:

```json
{
  "urId": 7639951412,
  "chainId": "eip155:5000",
  "symbol": "USD24",
  "type": "P2P",
  "transactionTypes": ["P2P", "FRX"],
  "pageSize": 20,
  "fromTimestamp": 1701234567,
  "toTimestamp": 1704234567,
  "direction": "ALL",
  "minAmount": "1000",
  "maxAmount": "100000",
  "status": "completed",
  "currencies": ["USD", "EUR"],
  "cursorTimestamp": 1702000000,
  "cursorId": 123456789,
  "prevCursorTimestamp": 1701990000,
  "prevCursorId": 123456700
}
```

| Field                                  | Type      | Required | Description                                                                                                      |
| -------------------------------------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `urId`                                 | int64     | Yes      | UR user ID                                                                                                       |
| `type`                                 | string    | No       | Single transaction type filter: P2P, FRX, CTU, CRD, CDP, CWD, CTF                                                |
| `transactionTypes`                     | \[]string | No       | Multiple transaction types filter (mutually exclusive with `type`, which only supports a single value)           |
| `chainId`                              | string    | No       | Chain ID (e.g., `eip155:5000`)                                                                                   |
| `symbol`                               | string    | No       | Token symbol (on-chain Token or fiat 24 series)                                                                  |
| `currencies`                           | \[]string | No       | Fiat identifier array: EUR, USD, CHF, CNH                                                                        |
| `pageSize`                             | int       | No       | Items per page, default 50, max 100                                                                              |
| `fromTimestamp` / `toTimestamp`        | int64     | No       | Time range filter, Unix seconds                                                                                  |
| `direction`                            | string    | No       | Transaction direction: IN, OUT, ALL                                                                              |
| `minAmount` / `maxAmount`              | string    | No       | Amount range filter (on-chain smallest unit string, follows amount description at the beginning of the document) |
| `status`                               | string    | No       | Transaction status filter (pending/completed/rejected/…)                                                         |
| `cursorTimestamp` / `cursorId`         | int64     | No       | Cursor for next page pagination (obtained from previous page's `nextCursor`)                                     |
| `prevCursorTimestamp` / `prevCursorId` | int64     | No       | Cursor for previous page pagination (optional)                                                                   |

**Response Example**:

```json
{
  "code": 0,
  "message": "",
  "data": [
    {
      "title": "Transfer to Alice",
      "subtitle": "#7639",
      "amount": "-100.50",
      "type": "P2P",
      "timestamp": 1704234567,
      "currency": "USD",
      "direction": "OUT",
      "txHash": "0x21b4...",
      "chainId": "eip155:5000",
      "inputToken": "USD24",
      "inputAmount": "100.50",
      "status": "completed"
    },
    {
      "title": "Card Payment",
      "subtitle": "Merchant ABC",
      "amount": "-36.90",
      "type": "CRD",
      "timestamp": 1704234000,
      "currency": "USD",
      "direction": "OUT",
      "txHash": "0x3051...",
      "chainId": "eip155:5000",
      "mcc": 5812,
      "status": "pending"
    }
  ],
  "hasNextPage": true,
  "hasPrevPage": false,
  "nextCursor": {
    "timestamp": 1704233500,
    "id": 99887766
  },
  "prevCursor": {
    "timestamp": 1704235000,
    "id": 99887700
  },
  "currentPageSize": 2
}
```

**Response Field Description**:

**Top-level fields**

* `code` / `message`: Status code and description, `0` indicates success.
* `data`: Transaction records array.
* `hasNextPage` / `hasPrevPage`: Whether next/previous page exists.
* `nextCursor` / `prevCursor`: Pagination cursors (next page uses `cursorTimestamp/cursorId`, previous page uses `prevCursorTimestamp/prevCursorId`).
* `currentPageSize`: Number of entries returned in current page.

**Each item in `data` array (same as `/v1/transaction/query`)**

* `title` / `subtitle`: Display title information.
* `amount`: Amount string with sign to indicate direction.
* `type`: Transaction type abbreviation (P2P, FRX, CTU, CRD, CDP, CWD, CTF, etc.).
* `timestamp`: Transaction occurrence Unix seconds.
* `currency`: Token (USD24, EUR24, etc.).
* `direction`: `IN` / `OUT`.
* `txHash`: Transaction hash (if applicable).
* `chainId`: CAIP-2 chain identifier (e.g., `eip155:5000`).
* `image`: Transaction display icon or card logo.
* `inputToken` / `inputAmount` / `inputTokenAddress`: Original chain info for cross-chain transactions like CTU, CSW.
* `outputAmount`: Amount after FRX/CSW exchange.
* `token` / `tokenAddress`: Token and contract address involved in certain card or aggregated transactions.
* `mcc`, `reference`, `bankAccount`: Fields specific to card payments or bank transfers.
* `crdMultiToken`, `ctuExternalSender`, `fromAddress`, `toAddress`, etc.: Additional fields attached by transaction type.
* `status`: `pending` / `completed` / `rejected` / `unknown`.
* `statusCode` / `crdCurrency`: Card-returned status code, original currency, and other supplementary info.
* `listingTitle`, `txHashUrl`, `txIdIcon`, `officialName`: Supplementary fields for client display.

## Fetch transaction details

**POST /v1/transaction/query**

Query aggregated transaction details based on on-chain transaction hash, supports single or multiple hashes.

The `txHash` field accepts both **EVM transaction hashes** (`0x`-prefixed hex) and **Solana transaction signatures** (Base58-encoded, 32–88 characters). The server auto-detects the format.

**Request Parameters**:

```json
{
  "txPairs": [
    {
      "urId": 7639951412,
      "txHash": "0x1234567890abcdef..."
    },
    {
      "urId": 7639951412,
      "txHash": "65ocipMENgErDtSxqo16J2JY8V2raAHeMLDgQNnRibbe5dc4JsENok34dWgcoh1qgBQwEi54jcSejgW6RsQB7S5s"
    }
  ]
}
```

| Field     | Type           | Required | Description                                                                                                      |
| --------- | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------- |
| `txPairs` | array\<object> | Yes      | Query pairs, each item contains `urId` + `txHash`. `txHash` accepts EVM hex (`0x…`) or Solana Base58 signatures. |

**Response Example**:

```json
{
  "code": 0,
  "message": "",
  "data": [
    // Each item follows the TransactionData structure, see "Public Data Structures" section
  ]
}
```

**Response Field Description**:

* `code`: Status code, `0` indicates success
* `message`: Status message, empty string `""` when successful
* `data`: Array of transaction records, each item follows the [TransactionData](/api-reference/account/delegated-contract-mode#transactiondata) structure

**Notes**:

* Returns `code=0` only if all pairs are successfully queried; if any pair has no record, returns `code=4001`, `message` is `transaction not found`
* Each item in the returned array has the same structure as a single record in the `/v1/transactions` list

## Create Sumsub access token by network

**Endpoint: POST /v1/create-access-token-by-network**

Creates a Sumsub SDK access token for a specified tokenId on a given network. This endpoint is used to initiate KYC verification for users on external (non-Mantle) networks. The partner ID is automatically embedded into the Sumsub external user ID for traceability.

**Prerequisites**:

* The partner must be whitelisted in Nacos (`ur-other-sumsub-allowed-partners`)
* The on-chain `walletProvider(tokenId)` must match the requesting partner ID

**Request Parameters**:

```json
{
  "tokenId": "100000",
  "network": "5000"
}
```

| Field     | Type   | Required | Description                                                  |
| --------- | ------ | -------- | ------------------------------------------------------------ |
| `tokenId` | string | Yes      | UR NFT token ID on the target network                        |
| `network` | string | Yes      | Chain ID (e.g., `"5000"` for Mantle, `"42161"` for Arbitrum) |

**Response Example**:

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "token": "sbx:aGVsbG8gd29ybGQ..."
  }
}
```

**Response Field Description**:

* `token`: Sumsub SDK access token, used to initialize the Sumsub WebSDK on the client side

**Error Codes**:

| Code    | Description                                                  |
| ------- | ------------------------------------------------------------ |
| `10001` | Partner not authenticated or not authorized                  |
| `20002` | Missing required parameters (`tokenId` or `network`)         |
| `20003` | `tokenId` does not belong to the requesting partner          |
| `50002` | Internal error (Sumsub client not configured or API failure) |

## Query Sumsub status by network

**Endpoint: POST /v1/sumsub-status-by-network**

Queries the current Sumsub KYC status for a specified tokenId on a given network. Returns the latest webhook status including review result and rejection details. Partners can only view records they created, or records with no partner association (legacy C-side data).

**Prerequisites**:

* The partner must be whitelisted in Nacos (`ur-other-sumsub-allowed-partners`)

**Request Parameters**:

```json
{
  "tokenId": "100000",
  "network": "5000"
}
```

| Field     | Type   | Required | Description                                                  |
| --------- | ------ | -------- | ------------------------------------------------------------ |
| `tokenId` | string | Yes      | UR NFT token ID on the target network                        |
| `network` | string | Yes      | Chain ID (e.g., `"5000"` for Mantle, `"42161"` for Arbitrum) |

**Response Example**:

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "type": "applicantWorkflowCompleted",
    "reviewStatus": "completed",
    "reviewAnswer": "GREEN",
    "applicantId": "65a1b2c3d4e5f6a7b8c9d0e1",
    "applicantType": "individual",
    "createdAtMs": 1714377600000
  }
}
```

**Response Field Description**:

* `type`: Sumsub webhook type (e.g., `applicantWorkflowCompleted`, `applicantReviewed`)
* `reviewStatus`: Review status (`init`, `pending`, `completed`, `onHold`)
* `reviewAnswer`: Review result (`GREEN` for approved, `RED` for rejected)
* `reviewRejectType`: Rejection type (only present when rejected, e.g., `FINAL`, `RETRY`)
* `levelName`: Sumsub verification level name
* `rejectLabels`: Array of rejection reason labels (only present when rejected)
* `applicantId`: Sumsub applicant ID
* `applicantType`: Applicant type (e.g., `individual`)
* `createdAtMs`: Record creation timestamp in milliseconds

**Error Codes**:

| Code    | Description                                          |
| ------- | ---------------------------------------------------- |
| `10001` | Partner not authenticated or not authorized          |
| `20002` | Missing required parameters (`tokenId` or `network`) |
| `50002` | Internal error (query failed)                        |

**Notes**:

* If no record exists for the given tokenId + network, returns `code=0` with an empty `data` object
* The query is scoped to the requesting partner's data: records created by other partners are not visible

## Apply USD payin

**Endpoint: POST /v1/apply-usd-payin**

Request a USD IBAN for a specific URId and return a request ID. UR does not issue USD IBANs automatically; call this endpoint whenever a user wants to receive USD. The call is synchronous: if the user is `Live`, UR creates the USD IBAN and returns success, with no manual review and no prior EUR or CHF pay-in required. Each user needs only one USD IBAN, so call this endpoint once per user.

**Request Parameters**:

```json
{
  "urId": 100000,
  "remark": "Key User"
}
```

| Field    | Type   | Required | Description     |
| -------- | ------ | -------- | --------------- |
| `urId`   | int64  | Yes      | UR NFT token ID |
| `remark` | string | Yes      | Request remark  |

**Response Example**:

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "applyId": 14563
  }
}
```

**Response Field Description**:

* `applyId`: Unique request ID

**Error Codes**:

| Code    | Description                                      |
| ------- | ------------------------------------------------ |
| `10001` | Partner not authenticated or not authorized      |
| `30027` | UrId already has an active USD Payin application |
| `50002` | UR Internal error                                |

**Notes**:

* The USD IBAN is a separate account number from the user's default EUR/CHF IBAN. UR issues the EUR/CHF IBAN automatically when the user reaches `Live`, but issues the USD IBAN only after you call this endpoint.
* After a successful call, retrieve the USD IBAN from the user profile, where `bankAccounts` is keyed by currency code. Read the `USD` entry for USD transfers, and match the IBAN you show the user to the currency they will send.

## Query CRS task status by tokenId

**Endpoint: POST /v1/crs-status-by-token**

Queries the current CRS Task status for a specified tokenId. Return whether a CRS task exists for the corresponding tokenId, along with the latest status of the CRS task.

**Request Parameters**:

```json
{
  "tokenId": "100000"
}
```

| Field     | Type   | Required | Description     |
| --------- | ------ | -------- | --------------- |
| `tokenId` | string | Yes      | UR NFT token ID |

**Response Example**:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "needCrs": true,
    "status": "submitted",
    "url": "https://fe2.qa4.gomantle.org/info-provide/crs?hash=AXUaIT3T&source=ur"
  }
}
```

**Response Field Description**:

* `needCrs`: Whether the user is required to perform a CRS task
* `status`: When the needCrs field is true, the status field indicates whether the user has submitted the CRS task: "submitted" means submitted, and "not\_submitted" means not submitted.
* `url`: The CRS submission link that directs the user to the CRS information collection page.

**Error Codes**:


# Signature and verify

Signing and verification rules for all API and webhook interactions.

This document defines the signing and verification rules for all parties interacting with the UR ecosystem. It serves as the single source of truth for signature formats and authentication mechanisms.

## Overview

The UR ecosystem uses two primary authentication methods depending on the API being accessed:

* **Partner Authentication (Server-to-Server)**: Used for `UR-OPEN-API` and Webhooks. Authenticates the Partner backend using a registered ECDSA key pair.
* **User Authentication (Wallet-to-Server)**: Used for `UR-API`. Authenticates individual end-users using their wallet's private key.

Both methods utilize **Ethereum Personal Sign (EIP-191)**: `"\x19Ethereum Signed Message:\n{len(messageToSign)}{messageToSign}"`

***

## Part A: Partner authentication (UR-OPEN-API & webhooks)

This method is used when a Partner backend calls UR APIs or when UR sends Webhooks to a Partner.

### 1. HTTP headers

| Header            | Description                                                                                                           | Required |
| ----------------- | --------------------------------------------------------------------------------------------------------------------- | -------- |
| `X-Api-Signature` | Hexadecimal signature with `0x` prefix.                                                                               | Yes      |
| `X-Api-Deadline`  | The Unix timestamp (in seconds) indicating when the request expires. Setting this to 5 minutes from the current time. | Yes      |
| `X-Api-PublicKey` | The Ethereum address of the signer.                                                                                   | Optional |

### 2. Signing logic

#### Partner request (Partner -> UR)

* **Signer**: Partner's registered backend Ethereum address.
* **Message Components**:
  * `requestBody`: The exact raw JSON string in the request body.
  * `deadline`: The value sent in the `X-Api-Deadline` header, a timestamp within the next 5 minutes.
* **Message to Sign**: Standard OpenAPI requests use `messageToSign = "{requestBody} {deadline}"` (note the single space between body and deadline). User-scoped FMA OpenAPI requests use `messageToSign = "{canonicalPayload}urId:{X-Ur-Id}externalUserId:{X-External-User-Id} {deadline}"`; see [API reference: Managed Custody Mode](https://docs.ur.app/api-reference/pages/gg7tFeofpTSL542LLbQS#id-2.2-authentication-partner-auth-eip-191).
* **Bodyless requests** (`GET` / `DELETE` / `HEAD`): there is no request body, so the raw **query string** takes the place of `requestBody`. The message becomes `messageToSign = "{rawQueryString} {deadline}"` (an empty message, meaning no query string, is rejected).
* **Signing code example:** Refers to [this](#signature).

#### UR response / webhook (UR -> Partner)

* **Signer**: UR Server address.
* **Message Components**:
  * `responseBody`: The exact raw JSON string returned in the HTTP body.
* **Message to Sign**: `messageToSign = "{responseBody}"`

### 3. Verification

The receiver must:

* Read the raw body and relevant headers.
* Construct the `messageToSign` as defined above.
* Use EIP-191 recovery to extract the signer's Ethereum address from the `X-Api-Signature`.
* Verify that the recovered address matches the expected/whitelisted address.

***

## Part B: User authentication (UR-API)

This method is used for sensitive user operations (e.g., FX, Transfers) where a direct wallet signature is required.

### 1. HTTP headers

| Header     | Description                                                                                                                         |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `sign`     | The user's wallet signature.                                                                                                        |
| `hash`     | A Keccak256 hash of the business payload or a unique message.                                                                       |
| `deadline` | The Unix timestamp (in seconds) indicating when the request expires. We recommend setting this to 20 minutes from the current time. |
| `tokenId`  | The user's UR Token ID (URID).                                                                                                      |

### 2. Signing logic

* **Construct Base Message**: `baseMessage = hash + deadline` (String concatenation).
* **Generate Intermediate Hash**: `intermediateHash = Keccak256(baseMessage)`.
* **Construct Final Message**: `finalMessage = "I agree to access my profile. " + intermediateHash.hex()`.
* **Sign**: The user signs the `finalMessage` using their wallet (EIP-191).

***

## Key pair

{% hint style="info" %}
If you use the [API sandbox](https://partner.ur.app/api-sandbox), it generates and registers this key pair for you. The steps below are for generating your own key pair instead. See [API signing key](https://docs.ur.app/getting-started/integration-guide#api-signing-key) for both paths.
{% endhint %}

### 1. Generate key pair using Node.js (viem)

```javascript
import { toHex } from "viem";
import {
  english,
  generateMnemonic,
  mnemonicToAccount,
} from "viem/accounts";

// Generate mnemonic and account
const mnemonic = generateMnemonic(english);
const account = mnemonicToAccount(mnemonic);
const privateKeyBytes = account.getHdKey().privateKey;

console.log("Address (Public Key):", account.address);
console.log("Private Key:", privateKeyBytes ? toHex(privateKeyBytes) : "<unavailable>");
```

### 2. Key management recommendations

Private Key Storage: Use key management services like AWS Secrets Manager, HashiCorp Vault Public Key Registration: Synchronize the generated address (publicKey) with the UR system through secure channels

***

## Code examples

### signature

```go
import (
    "fmt"
    "time"
    "strconv"
    "encoding/hex"
    "github.com/ethereum/go-ethereum/crypto"
)

// GenSignature generates API request signature
// Parameters:
//   privateKeyHex: private key hex string (without 0x prefix)
//   msg: business message content (typically the request body JSON string)
//   deadline: signature expiration time (Unix timestamp, seconds)
func GenSignature(privateKeyHex string, msg string, deadline int64) (string, error) {
    // 1. Build deadline string
    deadlineStr := strconv.FormatInt(deadline, 10)
    
    // For concurrent requests within the same second, you can add a very small random offset to the deadline (but still within the allowed window) to ensure signature uniqueness
    messageToSign := fmt.Sprintf("%s %s", msg, deadlineStr)
    
    // 2. Add Ethereum personal signature prefix
    prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(messageToSign))
    prefixedMessageBytes := []byte(prefix + messageToSign)
    
    // 3. Calculate message hash
    messageHash := crypto.Keccak256Hash(prefixedMessageBytes)
    
    // 4. Load private key and perform signature
    privateKey, err := crypto.HexToECDSA(privateKeyHex)
    if err != nil {
    return "", fmt.Errorf("failed to load private key: %w", err)
    }
    
    signatureBytes, err := crypto.Sign(messageHash.Bytes(), privateKey)
    if err != nil {
    return "", fmt.Errorf("failed to sign message: %w", err)
    }
    
    // 5. Return hex-encoded signature (including recovery ID)
    return "0x" + hex.EncodeToString(signatureBytes), nil
}
```

### verify

```go
// recoverAddress recovers signer's public key address from signature and original message
func recoverAddress(message, sigHex string) (common.Address, error) {
    // Decode hex signature
    sigBytes, err := hex.DecodeString(strings.TrimPrefix(sigHex, "0x"))
    if err != nil {
        return common.Address{}, fmt.Errorf("invalid hex signature: %w", err)
    }

    if len(sigBytes) != 65 {
        return common.Address{}, fmt.Errorf("signature length must be 65 bytes, got %d", len(sigBytes))
    }

    // Compatible handling of recovery ID (v): some libraries return 27/28, need to convert to 0/1
    if sigBytes[64] >= 27 {
        sigBytes[64] -= 27
    }

    // Rebuild prefixed message hash
    prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(message))
    prefixedMsgBytes := []byte(prefix + message)
    hash := crypto.Keccak256Hash(prefixedMsgBytes)

    // Recover public key from signature
    pubKey, err := crypto.SigToPub(hash.Bytes(), sigBytes)
    if err != nil {
        return common.Address{}, fmt.Errorf("could not recover public key: %w", err)
    }

    return crypto.PubkeyToAddress(*pubKey), nil
}

```

### User wallet signature

```javascript
// We only allow the signature to be valid for 20 minutes max.
// Can be less than that if want more security.
const SIGNATURE_DEADLINE_IN_SECONDS = 1200;

// Alternative to Date.now()
// sometimes the device clock is out of sync and can give the wrong timestamp
const serverTimestamp = await fetch("https://api.fiat24.com/timestamp");
const now = serverTimestamp.timestamp;

const deadline = Math.round(now / 1000) + SIGNATURE_DEADLINE_IN_SECONDS;

const hash = "Hello world"; // Could be any text or payload hash

// Calculate Hash of (hash + deadline)
// Example using web3.js
const deadlineHash = web3.utils.sha3(hash + deadline); 
// Example using ethers.js
// const deadlineHash = ethers.keccak256(ethers.toUtf8Bytes(hash + deadline));

// Construct the specific message required by UR
const messageToSign = `I agree to access my profile. ${deadlineHash}`;

// Generate Signature
// Example using web3.js
const sign = await web3.eth.personal.sign(messageToSign, address);
// Example using ethers.js
// const sign = await signer.signMessage(messageToSign);

return { hash, deadline, sign };

```

***

## Important notes

* **Raw Body**: Always use the raw HTTP body bytes. Do not re-serialize JSON, as key ordering or whitespace differences will cause signature mismatches.
* **Deadline Window**: Recommended window is 1-5 minutes. Requests with an expired deadline are rejected. A deadline too far in the future is also rejected, but the maximum forward window is **server-configured** (typically 5 minutes) and is only enforced when that limit is set; it is not a hard-coded 5-minute constant.
* **Replay Protection**: For high-concurrency environments, add a small random offset (1-5 seconds) to the `deadline` to ensure each request generates a unique signature.
* **Case Sensitivity**: Ethereum addresses should be treated as case-insensitive but are typically stored/transmitted in lowercase or checksum format.

## Environment addresses

| Environment | UR Server Signature Address                  |
| ----------- | -------------------------------------------- |
| **Sepolia** | `0x4D2AA3f43De8f8BE746E315D291B804a4aBD3939` |
| **Mainnet** | `0xee28dEaD5F114C8405BE3be1144D59A4110B7F79` |


# Development environment

The sandbox is an isolated environment for testing and development, without affecting production.

## Testnet

UR provides a sandbox environment for integration development and testing.

**Base URL:** `https://urapi2-qa.ur-inc.xyz`

{% hint style="warning" %}
**KYC data on testnet is mocked.** Identity verification results are simulated and do not reflect real compliance outcomes. After completing the KYC flow on testnet, your test users will not be automatically approved; contact **<support@ur.app>** or your dedicated integration channel to have test accounts manually moved to `Live` status.
{% endhint %}

## Two API surfaces

UR exposes two API surfaces:

### UR-OPEN-API (Partner Server → UR Server)

Server-to-server calls authenticated with your **partner wallet signature**. Use these for operations that apply to both integration modes:

* Minting URIDs
* Querying user profiles and balances
* Fetching transaction history

**Base URL (production):** `https://openapi.ur.app`

### UR-API (mode-specific endpoints)

Endpoints specific to your chosen integration mode:

* **Managed Custody Mode:** Off-ramp, on-ramp (coming soon), FX, payout, card; all via partner-signed REST calls
* **External Wallet Access:** User registration, KYC flow, card, off-ramp, on-ramp (coming soon), FX, payouts

**Base URLs (production):**

| Mode                   | URL                      |
| ---------------------- | ------------------------ |
| Managed Custody Mode   | `https://openapi.ur.app` |
| External Wallet Access | `https://api.ur.app`     |

## Response format

All endpoints return JSON with this structure:

```json
{
  "code": 0,
  "message": "success",
  "data": { }
}
```

A `code` of `0` indicates success. Non-zero codes indicate errors.


# Smart contracts

Contract addresses, source code, audit reports, and on-chain interaction reference.

## Source code & audits

* **Contract Source Code**: <https://github.com/ur-app/ur-contracts>
* **Audit Reports**: <https://github.com/ur-app/ur-contracts/tree/main/Audits>

## Environment

Most of UR's core functionalities, including account management and card operations, are deployed and executed exclusively on the **Mantle Network**. Interactions with other chains (such as Ethereum, Arbitrum, Base, Monad, and Tempo) are only required for specific features like **Deposit** and **Cash to Crypto**.

For deployment, we recommend using [QuickNode](https://www.quicknode.com/) or [Alchemy](https://www.alchemy.com/) as your RPC provider.

### Mantle mainnet

* **Chain ID**: 5000
* **RPC Endpoint**: <https://rpc.mantle.xyz>

### Mantle Sepolia testnet

* **Chain ID**: 5003
* **RPC Endpoint**: <https://rpc.sepolia.mantle.xyz>

### Chain explorers

| Network      | Mainnet                      | Testnet                              |
| ------------ | ---------------------------- | ------------------------------------ |
| Mantle       | <https://mantlescan.xyz/>    | <https://sepolia.mantlescan.xyz/>    |
| Ethereum     | <https://etherscan.io/>      | <https://sepolia.etherscan.io/>      |
| Arbitrum One | <https://arbiscan.io/>       | <https://sepolia.arbiscan.io/>       |
| Base         | <https://basescan.org/>      | <https://sepolia.basescan.org/>      |
| BSC          | <https://bscscan.com/>       | <https://testnet.bscscan.com/>       |
| Monad        | <https://monadscan.com/>     | <https://testnet.monadscan.com/>     |
| Tempo        | <https://explore.tempo.xyz/> | <https://explore.testnet.tempo.xyz/> |

## Functions

### Account

Our account model is based on the ERC721 NFT standard; each user has a unique UR ID (Token ID) that identifies and manages their assets and permissions on the UR platform.\
The user's KYC status is recorded in the contract's `status` mapping, with each status corresponding to a numeric value as follows:

#### **Contract addresses**

| Network | Address                                                                                                                           |
| ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Mainnet | [`0x4a05148119683E0A41b52fb973EEF0EE81536c47`](https://mantlescan.xyz/address/0x4a05148119683E0A41b52fb973EEF0EE81536c47)         |
| Testnet | [`0xfE6fB4aE524c8f032E14691C3B2465cc5bcB9677`](https://sepolia.mantlescan.xyz/address/0xfE6fB4aE524c8f032E14691C3B2465cc5bcB9677) |

#### **Methods**

| Function Name         | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ownerOf`             | Query EVM wallet address by URID.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `tokenOfOwnerByIndex` | Query URID by EVM wallet address. This ERC721Enumerable method returns the token ID owned by an address at a given index. Since each user should hold one Account NFT, use `index = 0` to get the user's URID.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `status`              | <p>Status: KYC passed, status is <code>Live</code>. Functions for <code>non-Live</code> statuses are restricted.<br><br><code>SoftBlocked, // 1</code><br><code>Tourist, // 2</code><br><code>Blocked, // 3</code><br><code>Closed, // 4</code><br><code>Live // 5</code></p>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `limit`               | <p>Query user monthly transaction limit by URID.<br>All outgoing Fiat transactions count towards this limit.<br>For KYC-verified users, the default monthly limit is <strong>30,000 CHF</strong>.<br><br>Returns <code>usedLimit</code> (uint256), <code>clientLimit</code> (uint256), <code>startLimitDate</code> (uint256).<br><br>Users receive a spending limit that refreshes every 30 days.<br><br>- <strong>usedLimit</strong> (<code>uint256</code>): Amount of the limit already used in the <em>current</em> 30-day cycle. This value is reset every 30 days.<br>- <strong>clientLimit</strong> (<code>uint256</code>): Total allowed limit for the current 30-day period.<br>- <strong>startLimitDate</strong> (<code>uint256</code>): Timestamp of the last limit reset (start time of the current 30-day cycle).</p> |

#### **ABI**

[Fiat24Account.sol](https://github.com/ur-app/ur-contracts/blob/main/src/Fiat24Account.sol)

#### Example: get URID by wallet address (TypeScript + viem)

```typescript
import { createPublicClient, http, parseAbi } from 'viem'
import { mantle } from 'viem/chains'

const client = createPublicClient({
  chain: mantle,
  transport: http('https://rpc.mantle.xyz')
})

const abi = parseAbi([
  'function tokenOfOwnerByIndex(address owner, uint256 index) view returns (uint256)'
])

const accountContractAddress = '0x4a05148119683E0A41b52fb973EEF0EE81536c47'
const userAddress = '0x...' // Replace with the user's wallet address

async function getUrid() {
  try {
    const tokenId = await client.readContract({
      address: accountContractAddress,
      abi: abi,
      functionName: 'tokenOfOwnerByIndex',
      args: [userAddress, 0n]
    })

    console.log(`URID: ${tokenId.toString()}`)
  } catch (error) {
    console.error('Error fetching URID:', error)
  }
}

getUrid()
```

### Fiat balance

#### **Contract addresses**

| Currency | Mainnet                                                                                                                   | Testnet                                                                                                                           |
| -------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| USD      | [`0xD598839598bBF508b97697b7D9e80054D4bcaaCC`](https://mantlescan.xyz/address/0xD598839598bBF508b97697b7D9e80054D4bcaaCC) | [`0xdf79470986629ae4893BfCE0c6C0F4d085E99741`](https://sepolia.mantlescan.xyz/address/0xdf79470986629ae4893BfCE0c6C0F4d085E99741) |
| EUR      | [`0x0578be9C858e6562dd8cd11a738b89Ca48194dA5`](https://mantlescan.xyz/address/0x0578be9C858e6562dd8cd11a738b89Ca48194dA5) | [`0x5E52c8993283023B83e87eF577f7f51Fa1c5B007`](https://sepolia.mantlescan.xyz/address/0x5E52c8993283023B83e87eF577f7f51Fa1c5B007) |
| CHF      | [`0x53587A05ccDdCE555C2Cd7cE4C9c5Bc3D912E2f3`](https://mantlescan.xyz/address/0x53587A05ccDdCE555C2Cd7cE4C9c5Bc3D912E2f3) | [`0x52837070C96C6D5E23ed90a43479c0237c41864c`](https://sepolia.mantlescan.xyz/address/0x52837070C96C6D5E23ed90a43479c0237c41864c) |
| CNH      | [`0xa0af0C397CB0A52F5E8Bc7BB89068dDDfaE9F211`](https://mantlescan.xyz/address/0xa0af0C397CB0A52F5E8Bc7BB89068dDDfaE9F211) | [`0x4AfbC767e6d310296657b7759a5F2c303F26B327`](https://sepolia.mantlescan.xyz/address/0x4AfbC767e6d310296657b7759a5F2c303F26B327) |
| SGD      | [`0x8F7F92F2A0247cc8660C4C4EF69582Bc6849B4d9`](https://mantlescan.xyz/address/0x8F7F92F2A0247cc8660C4C4EF69582Bc6849B4d9) | [`0x2FEb2d95ce8eC88931c4031cdd2875C90EDC87FE`](https://sepolia.mantlescan.xyz/address/0x2FEb2d95ce8eC88931c4031cdd2875C90EDC87FE) |
| JPY      | [`0x3bC9fC0460cAC2DdD352848ECc0BFe204c220717`](https://mantlescan.xyz/address/0x3bC9fC0460cAC2DdD352848ECc0BFe204c220717) | [`0x8af3be43607cb1e57b6e37fda99b6b988c5b48f0`](https://sepolia.mantlescan.xyz/address/0x8af3be43607cb1e57b6e37fda99b6b988c5b48f0) |
| HKD      | [`0x64266a15432004708e5fCA0239f664d069853374`](https://mantlescan.xyz/address/0x64266a15432004708e5fCA0239f664d069853374) | [`0x30c94bCF88c5c8f3Ff08F38242a38C656Bb28a6d`](https://sepolia.mantlescan.xyz/address/0x30c94bCF88c5c8f3Ff08F38242a38C656Bb28a6d) |

#### **Methods**

| Method      | Description                                                                                                                                                                                                     |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `balanceOf` | Query a user's fiat token balance by their address.                                                                                                                                                             |
| `decimals`  | Precision of the token. For example, if `decimals` equals `2`, a balance of `505` tokens should be displayed to a user as `5.05` (`505 / 10 ** 2`).                                                             |
| `allowance` | This function can be used to query the amount a user has approved for a given spender. Generally, for an unlimited approval, we call the approve method with value set to the maximum of uint256, i.e. 2^256-1. |

#### **ABI**

[Fiat24Token.sol](https://github.com/ur-app/ur-contracts/blob/main/src/Fiat24Token.sol)

#### Example: get balance (TypeScript + viem)

```typescript
import { createPublicClient, http, parseAbi, formatUnits } from 'viem'
import { mantle } from 'viem/chains'

// 1. Create a client connected to Mantle Mainnet
const client = createPublicClient({
  chain: mantle,
  transport: http('https://rpc.mantle.xyz')
})

// 2. Define the ABI for balanceOf and decimals
const abi = parseAbi([
  'function balanceOf(address owner) view returns (uint256)',
  'function decimals() view returns (uint8)'
])

// 3. Contract and User addresses
const eurContractAddress = '0x0578be9C858e6562dd8cd11a738b89Ca48194dA5' // EUR on Mantle Mainnet
const userAddress = '0x...' // Replace with the user's wallet address

async function getBalance() {
  try {
    // 4. Read contract data
    const [balance, decimals] = await Promise.all([
      client.readContract({
        address: eurContractAddress,
        abi: abi,
        functionName: 'balanceOf',
        args: [userAddress]
      }),
      client.readContract({
        address: eurContractAddress,
        abi: abi,
        functionName: 'decimals'
      })
    ])

    // 5. Format and display the balance
    console.log(`Raw Balance: ${balance}`)
    console.log(`Decimals: ${decimals}`)
    console.log(`Formatted Balance: ${formatUnits(balance, decimals)} EUR`)
    
  } catch (error) {
    console.error('Error fetching balance:', error)
  }
}

getBalance()
```

### Money transfer

#### **Contract addresses**

**Contract refers to the** [**currency contracts**](#contract-address-1)**.**

#### **Methods**

> **Note:** Only users in **Live** status (`status == 5`) are allowed to transfer their fiat tokens out in any form.

| Method                | Description                                                                                                                                                                                                                           |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transfer`            | Transfer fiat token to any EVM address.                                                                                                                                                                                               |
| `transferByAccountId` | Transfer fiat tokens to the target user by their URID.                                                                                                                                                                                |
| `clientPayout`        | <p>User to transfer their currency out via bank transfer to a UR-supported bank account.<br>Before the user calls this contract function, a payout account (contactId) must be created through UR's API (The API is coming soon).</p> |

#### Example: transfer fiat (TypeScript + viem)

```typescript
import { createWalletClient, http, parseAbi, parseUnits } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mantle } from 'viem/chains'

// 1. Setup Wallet Client (Required for write operations)
// ⚠️ NEVER hardcode private keys in production code
const account = privateKeyToAccount('0x...') 
const client = createWalletClient({
  account,
  chain: mantle,
  transport: http('https://rpc.mantle.xyz')
})

// 2. Define ABI for transfer
const abi = parseAbi([
  'function transfer(address to, uint256 amount) returns (bool)'
])

// 3. Configuration
const eurContractAddress = '0x0578be9C858e6562dd8cd11a738b89Ca48194dA5' // EUR Contract
const recipientAddress = '0x...' // Receiver's address
const amountToSend = '10' // Amount in EUR

async function transferFiat() {
  try {
    // 4. Send Transaction
    // Note: Ensure the sender has enough balance and ETH(MNT) for gas
    const hash = await client.writeContract({
      address: eurContractAddress,
      abi: abi,
      functionName: 'transfer',
      args: [
        recipientAddress, 
        parseUnits(amountToSend, 2) // Assuming 2 decimals for EUR
      ]
    })

    console.log(`Transaction sent! Hash: ${hash}`)
  } catch (error) {
    console.error('Transfer failed:', error)
  }
}

transferFiat()
```

### Money exchange

Also known as foreign-exchange conversion, used for exchanging between fiat currencies.

#### **Contract addresses**

| Network                | Address                                                                                                                           |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Mantle Mainnet         | [`0x9F88e04D129d4a4247F009833ba0Bd5D8F6A2146`](https://mantlescan.xyz/address/0x9F88e04D129d4a4247F009833ba0Bd5D8F6A2146)         |
| Mantle Sepolia Testnet | [`0x2C2E6BC745e583629a4157c2D8a9234d59F4067e`](https://sepolia.mantlescan.xyz/address/0x2C2E6BC745e583629a4157c2D8a9234d59F4067e) |

#### **Methods**

> **Note:** Only users in **Live** status (`status = 5`) are allowed to do money exchange.

| Method                 | Description                                                             |
| ---------------------- | ----------------------------------------------------------------------- |
| `moneyExchangeExactIn` | Exchange between 2 currencies                                           |
| `getExchangeRate`      | Query the exchange rate between two specified currency token addresses. |

#### **ABI**

[Fiat24CryptoRelay.sol](https://github.com/ur-app/ur-contracts/blob/main/src/Fiat24CryptoRelay.sol)

#### Example: money exchange (TypeScript + viem)

This process involves two steps:

1. Query the current exchange rate using `getExchangeRate`.
2. Execute the exchange using `moneyExchangeExactin`.

```typescript
import { createPublicClient, createWalletClient, http, parseAbi, parseUnits, formatUnits } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { mantle } from 'viem/chains'

// 1. Setup Clients
const account = privateKeyToAccount('0x...')
const publicClient = createPublicClient({
  chain: mantle,
  transport: http('https://rpc.mantle.xyz')
})
const walletClient = createWalletClient({
  account,
  chain: mantle,
  transport: http('https://rpc.mantle.xyz')
})

// 2. Define ABI
const abi = parseAbi([
  'function getExchangeRate(address srcToken, address destToken) view returns (uint256)',
  'function moneyExchangeExactin(address srcToken, address destToken, uint256 srcAmount, uint256 minDestAmount) returns (uint256)'
])

// 3. Configuration
const exchangeContractAddress = '0x9F88e04D129d4a4247F009833ba0Bd5D8F6A2146'
const eurAddress = '0x0578be9C858e6562dd8cd11a738b89Ca48194dA5' // Source: EUR
const usdAddress = '0xD598839598bBF508b97697b7D9e80054D4bcaaCC' // Destination: USD
const amountInEur = '100' // Exchange 100 EUR

async function exchangeCurrency() {
  try {
    // 4. Step 1: Get Exchange Rate
    const rate = await publicClient.readContract({
      address: exchangeContractAddress,
      abi: abi,
      functionName: 'getExchangeRate',
      args: [eurAddress, usdAddress]
    })
    console.log(`Current Rate: ${formatUnits(rate, 18)}`) // Rate has 18 decimals

    // 5. Calculate Minimum Destination Amount (Slippage Protection)
    // Example: Allow 1% slippage
    const srcAmount = parseUnits(amountInEur, 2) // EUR has 2 decimals
    const expectedDestAmount = (srcAmount * rate) / parseUnits('1', 18)
    const minDestAmount = (expectedDestAmount * 99n) / 100n 

    // 6. Step 2: Execute Exchange
    const hash = await walletClient.writeContract({
      address: exchangeContractAddress,
      abi: abi,
      functionName: 'moneyExchangeExactin',
      args: [eurAddress, usdAddress, srcAmount, minDestAmount]
    })

    console.log(`Exchange submitted! Hash: ${hash}`)
  } catch (error) {
    console.error('Exchange failed:', error)
  }
}

exchangeCurrency()
```

### Deposit (off-ramp)

#### **Contract addresses**

| Network      | Mainnet                                                                                                                                   | Testnet                                                                                                                                            |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Mantle       | [`0xd08B421A33F9b09A59E2ebf72afEF2365ce5b083`](https://mantlescan.xyz/address/0xd08B421A33F9b09A59E2ebf72afEF2365ce5b083)                 | [`0xd6d4C6eB84697cB9bf0045e88b8f3A0bD42D3d66`](https://sepolia.mantlescan.xyz/address/0xd6d4C6eB84697cB9bf0045e88b8f3A0bD42D3d66)                  |
| Arbitrum     | [`0xd08B421A33F9b09A59E2ebf72afEF2365ce5b083`](https://arbiscan.io/address/0xd08B421A33F9b09A59E2ebf72afEF2365ce5b083#readProxyContract)  | [`0xCa8eFFac628001B86e068b8367174F91E7E88357`](https://sepolia.arbiscan.io/address/0xCa8eFFac628001B86e068b8367174F91E7E88357)                     |
| Ethereum     | [`0xd08B421A33F9b09A59E2ebf72afEF2365ce5b083`](https://etherscan.io/address/0xd08B421A33F9b09A59E2ebf72afEF2365ce5b083#readProxyContract) | [`0xF67b89d376D3aAA872E151B4526271D9394aE192`](https://sepolia.etherscan.io/address/0xF67b89d376D3aAA872E151B4526271D9394aE192)                    |
| Base Network | [`0xd08B421A33F9b09A59E2ebf72afEF2365ce5b083`](https://basescan.org/address/0xd08B421A33F9b09A59E2ebf72afEF2365ce5b083)                   | [`0xd6d4C6eB84697cB9bf0045e88b8f3A0bD42D3d66`](https://sepolia.basescan.org/address/0xd6d4C6eB84697cB9bf0045e88b8f3A0bD42D3d66#writeProxyContract) |
| Monad        | [`0xd08B421A33F9b09A59E2ebf72afEF2365ce5b083`](https://monadscan.com/address/0xd08B421A33F9b09A59E2ebf72afEF2365ce5b083)                  | [`0x886DCF3BCb4fe8b2b07366C9F9aEbD3e471E8abA`](https://testnet.monadscan.com/address/0x886DCF3BCb4fe8b2b07366C9F9aEbD3e471E8abA)                   |
| Tempo        | [`0xd69016A2C64cF7f075F8644b96419421afd44e71`](https://explore.tempo.xyz/address/0xd69016A2C64cF7f075F8644b96419421afd44e71)              | [`0xe20C2CF90f1e4AA8ABE20a6562C16B3601ad29BF`](https://explore.testnet.tempo.xyz/address/0xe20C2CF90f1e4AA8ABE20a6562C16B3601ad29BF)               |

#### **Methods**

> Note: Only users with status **Live (5)** or **SoftBlocked (1)** can perform deposit(off-ramp) transactions.

| Method                               | Description                                                                                                                                                                                                                                                                                 |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `depositTokenViaUsdc`                | The user deposits USDC into the contract and receives fiat currency in their UR account.                                                                                                                                                                                                    |
| `depositTokenViaAggregator`          | <p>Supports users depositing a specific crypto asset and receiving fiat currency in their UR account.<br><br><em>The prerequisite is that the frontend must integrate a DEX/aggregator API(API is coming soon) and construct the Crypto → USDC swap as calldata.</em></p>                   |
| `depositTokenViaAggregatorToAccount` | Same as `depositTokenViaAggregator`, with one extra parameter `_targetAccount`: the user's UR Account address that receives the resulting fiat. Used when the signing Crypto Wallet is **not** the user's UR Account itself (e.g., a Partner-side external wallet in Managed Custody Mode). |
| `depositWithFee`                     | Tempo chain only. The user deposits a supported stablecoin into the contract and receives fiat currency in their UR Account. No external DEX aggregator is required.                                                                                                                        |
| `depositWithFeeTo`                   | Tempo chain only. Same as `depositWithFee`, with one extra parameter `recipient`: the address that receives the resulting fiat. Used when the signing wallet is not the user's UR Account itself.                                                                                           |

#### **ABI**

[Fiat24CryptoDeposit.sol](https://github.com/ur-app/ur-contracts/blob/main/src/Fiat24CryptoDeposit.sol)

[Fiat24CryptoDeposit\_Tempo.sol](https://github.com/ur-app/ur-contracts/blob/main/src/Fiat24CryptoDeposit_Tempo.sol)

### Onramp

{% hint style="warning" %}
**Available soon.** On-ramp (fiat-to-crypto) is not yet available for integration and will be enabled in a future release. The reference below is provided for preview only.
{% endhint %}

#### **Contract addresses**

| Network                    | Mainnet                                                                                                                      | Testnet                                                                                                                              |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Mantle                     | [`0x2460634bf887A0F4E885278B93E10f91D48a5a8c`](https://mantlescan.xyz/address/0x2460634bf887A0F4E885278B93E10f91D48a5a8c)    | [`0x9291da7e1807bc6Ea230241fFd5BC25E2f105DAB`](https://sepolia.mantlescan.xyz/address/0x9291da7e1807bc6Ea230241fFd5BC25E2f105DAB)    |
| Arbitrum/Base/Ethereum/BSC | `0xAACe017F0a6Bb9890E449d5b27fbcA9C440b81e9`                                                                                 |                                                                                                                                      |
| Tempo                      | [`0xb4aB250c3Bf849B3C9529AE7dac3c97267febF92`](https://explore.tempo.xyz/address/0xb4aB250c3Bf849B3C9529AE7dac3c97267febF92) | [`0xc88AC8d0c118D231E5E88Ac6819a4E546457b1B6`](https://explore.testnet.tempo.xyz/address/0xc88AC8d0c118D231E5E88Ac6819a4E546457b1B6) |

> **Note:** Partners only need the **Mantle** address. Onramp is initiated and settled against the user's fiat balance on Mantle. The contracts on the other chains (Arbitrum, Base, Ethereum, BSC, Tempo) run automatically: after the funds bridge out, the swap on the destination chain is triggered by the contract itself through LayerZero. Partners never call the non-Mantle addresses directly, so those addresses are listed for reference only.

#### **Methods**

#### **ABI**

[BufferPool.sol](https://github.com/ur-app/ur-contracts/blob/main/src/BufferPool.sol)

### Card

#### **Contract addresses**

**Contract refers to the** [**currency contracts**](#contract-address-1)**.**

**Spender Contract (Card Authorization):**

| Network | Address                                                                                                                           |
| ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Mainnet | [`0xb9d38DDE25f67D57af5b91C254F869F90d483d05`](https://mantlescan.xyz/address/0xb9d38DDE25f67D57af5b91C254F869F90d483d05)         |
| Testnet | [`0x25d66C564532258eD9cdBB6215E260AFf41d8bae`](https://sepolia.mantlescan.xyz/address/0x25d66C564532258eD9cdBB6215E260AFf41d8bae) |

#### Methods

To use the UR debit card, users must authorize the **Spender Contract** to spend their **Fiat** or **USDe** balance via `approve` or `permit` methods.

| Method               | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `approve` / `permit` | <p>Authorize the <strong>Spender Contract</strong> to spend funds.<br><br>The debit card payments are booked by the Spender contract via the <code>authorize()</code> function. Therefore, the card owner needs to approve a certain limit to the Spender contract for future card spendings.<br><br><strong>Supported Assets:</strong><br>- <strong>Fiat Tokens:</strong> Users must approve the corresponding Fiat token contract.<br>- <strong>USDe:</strong> Users can also authorize USDe directly for consumption.<br><br><strong>Usage Notes:</strong><br>- <strong>Enable Authorization:</strong> It is common practice to approve the maximum <code>uint256</code> value (<code>2^256 - 1</code>) to ensure uninterrupted service.<br>- <strong>Revoke Authorization:</strong> To cancel the authorization, call the <code>approve</code> method with an amount of <code>0</code>.</p> |

#### **ABI**

[Fiat24Token.sol](https://github.com/ur-app/ur-contracts/blob/main/src/Fiat24Token.sol)

#### Card transaction flow

![Card Transaction Flow](/files/W751uXZkIj6dW6dQBFB6)


# External Wallet Access Mode

Complete API reference for partners integrating via External Wallet Access.

## 1. Overview

The UR API consists of two distinct services hosted on independent domains: **UR-API** (UR's private API) and **UR-OPEN-API** (Open API for third parties).

* **UR-API:** Provides functionalities for User Registration, KYC Verification, Crypto Deposits, Foreign Exchange (FX), and Crypto On/Off Ramps.
* **UR-OPEN-API:** Provides Third-party Authentication, Corporate (B-side) Minting, and Corporate Transaction History queries.

Sections 1.1 and 1.2 introduce the base information and authentication methods for **UR-API** (Client-side). Section 8 introduces the base information and authentication methods for **UR-OPEN-API** (Partner-side).

### 1.1 API base information

#### Base URLs

The API is deployed across different environments. Use the appropriate base URL for your integration stage.

| Environment    | Base URL                       |
| -------------- | ------------------------------ |
| **Testnet**    | `https://urapi3-qa.ur-inc.xyz` |
| **Preview**    | `https://api2-preview.ur.app`  |
| **Production** | `https://api.ur.app`           |

#### Authentication method: header authentication

All API requests must include the following specific headers for authentication and verification .

| Header Field | Type   | Required | Description                                |
| ------------ | ------ | -------- | ------------------------------------------ |
| `tokenId`    | string | **Yes**  | User's URID                                |
| `network`    | string | **Yes**  | The blockchain network identifier.         |
| `sign`       | string | **Yes**  | The cryptographic signature from the user. |
| `hash`       | string | **Yes**  | The hash of the original request data.     |
| `deadline`   | string | **Yes**  | The expiration timestamp for the request.  |

**Field Definitions** :

* **tokenId**: The User's URID.
* **network**: Use `5000` for Mainnet and `5003` for Testnet.
* **sign**: The signature generated by the user's wallet.
* **hash**: The SHA3/Keccak256 hash of the original request payload.
* **deadline**: Server timestamp + validity window (max 20 minutes). This is controlled by the partner to ensure signature validity.

#### Data format

* **Format**: JSON

#### Universal response format

All API responses follow this standard JSON structure :

```json
{
  "retCode": 0, // Return Code. 0 indicates success; non-0 indicates failure.
  "retMsg": "success", // Return Message. Contains error details if retCode != 0.
  "result": {}, // Result Data. The specific business data (JSON object).
  "timeNow": 1703123456789 // Server Timestamp.
}
```

### 1.2 Signature and verify

UR APIs are categorized into three authentication levels based on data sensitivity and operation type. For a complete guide on signature formats and verification logic for both Partners and Users, please refer to [Signature and Verification](/api-reference/signature-and-verify).

#### A. No auth (public interfaces)

* **Scenarios**: Querying public information such as supported countries, UR server timestamp, exchange rates, or OTP login.
* **Method**: No authentication information is required in the request.

#### B. Basic auth

**Scenarios**: Operations that do not involve fund movements but require basic user identification, such as User Registration (Minting) or generating tokens. **Method**: The request must include the `tokenId`(User's URID) in the Header.

#### C. Full auth

* **Scenarios**: Operations involving fund movements or sensitive data where every call requires a user signature to prevent replay attacks. Examples include Currency Exchange (FX), Permit, and querying Transaction History.

> **Note**: In the following documentation, every API will indicate its specific **Authentication Level**. Callers must strictly adhere to these levels. Additionally, some interfaces may have specific requirements regarding the User Status.

## 2. Account

This section covers API endpoints and webhooks related to user account management.

### 2.1 Account API

#### 2.1.1 Get server timestamp

**1. Description** Retrieves the current server timestamp from the UR service. This timestamp is required for calculating the signature `deadline`.

**2. Request**

| Item            | Value               | Note                      |
| --------------- | ------------------- | ------------------------- |
| **HTTP Method** | `GET`               |                           |
| **URI**         | `/api/v1/timestamp` |                           |
| **Auth Level**  | **No Auth**         |                           |
| **Headers**     | `Content-Type`      | Fixed: `application/json` |

|

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": "{}",
  "timeNow": 1703123456789
}
```

**4. Request Example**

```bash
curl 'https://get.ur.app/api/v1/timestamp' \
-H 'content-type: application/json; charset=UTF-8'

```

**5. Error Codes**

#### 2.1.2 Email verification

**1. Description** Checks if the user's email address is available for registration. If the email is unavailable, the interface returns an error message, and the user must provide a different email to open an account.

**2. Request**

| Item            | Value                  | Note |
| --------------- | ---------------------- | ---- |
| **HTTP Method** | `POST`                 |      |
| **URI**         | `/api/v2/email-status` |      |
| **Auth Level**  | **No Auth**            |      |

**Request Parameters**

```json
{
  "email": "xxx@gmail.com"
}
```

**3. Response**

**Scenario: Email already exists (Unavailable)**

```json
{
  "retCode": 10019,
  "retMsg": "Email already registered, please use a different email",
  "result": null,
  "timeNow": 0
}
```

**Scenario: Email is available (Success)**

```json
{
  "retCode": 0,
  "retMsg": "OK",
  "result": null,
  "timeNow": 0
}
```

**4. Request Example**

```bash
curl -X POST http://localhost:8888/api/v2/email-status \
--data '{"email": "abc@gmail.com"}'

```

**5. Error Codes**

#### 2.1.3 Get user status

**1. Description** Retrieves detailed status information of the user account, including account status, KYC process progress, SumSub KYC info, and novice guidance progress .

**Scenarios:**

* Query account status after user login.
* Determine accessible features based on current status.
* Display the current progress of the KYC verification flow.
* Track novice guidance completion.
* Determine if the user needs to complete specific verification steps .

**2. Request**

| Item            | Value                    | Note                      |
| --------------- | ------------------------ | ------------------------- |
| **HTTP Method** | `GET`                    |                           |
| **URI**         | `/api/v2/account-status` |                           |
| **Auth Level**  | **Basic Auth**           | No status restriction     |
| **Headers**     | `Content-Type`           | Fixed: `application/json` |
|                 | `tokenId`                | User's URID               |
|                 | `network`                | Network Identifier        |

**3. Response**

**Success Response (HTTP 200):**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": "{\"status\":5,\"statusStr\":\"Live\",\"kycFlow\":{\"currentStep\":4,\"currentStepStr\":\"Review\",\"currentStepActionTypes\":[],\"failReason\":\"\"},\"sumsubKycInfo\":{\"userId\":\"app123456\",\"latestKycHasCompleted\":true,\"reviewStatus\":\"completed\",\"reviewAnswer\":\"GREEN\",\"reviewRejectType\":\"\"},\"noviceGuidanceProcessStep\":5,\"noviceGuidanceProcessStepLatestTimeMs\":1703123456789,\"crsInfo\":{\"needCrs\":true,\"restrictDate\":1778402365685,\"url\":\"https://ur-fe2.qa4.gomantle.org/info-provide/crs?hash=D9cEvsYk&source=ur\"}}",
  "timeNow": 1703123456789
}
```

`**result` Field Breakdown:

| Field Name                              | Type   | Description                                                                                          |
| --------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------- |
| `status`                                | int    | Account status code (`0`=Na, `1`=SoftBlocked, `2`=Tourist, `3`=Blocked, `4`=Closed, `5`=Live)        |
| `statusStr`                             | string | Account status string ("Na", "SoftBlocked", "Tourist", "Blocked", "Closed", "Live".)                 |
| `kycFlow`                               | object | KYC flow information (see `KycFlowResponse` structure below)                                         |
| `sumsubKycInfo`                         | object | SumSub KYC information (see `SumsubKycInfo` structure below)                                         |
| `noviceGuidanceProcessStep`             | int    | Current step of novice guidance (0 indicates not started or unfinished; queried only in Live status) |
| `noviceGuidanceProcessStepLatestTimeMs` | int64  | Latest update timestamp for novice guidance (milliseconds)                                           |

`**KycFlowResponse` Structure:

| Field Name               | Type    | Description                                                                                    |
| ------------------------ | ------- | ---------------------------------------------------------------------------------------------- |
| `currentStep`            | int     | Current KYC step (`0`=UNKNOWN, `1`=FormA, `2`=IDScan, `3`=SignFormA, `4`=Review, `5`=Rejected) |
| `currentStepStr`         | string  | Current step name ("FormA", "IDScan", "SignFormA", "Review", "Rejected")                       |
| `currentStepActionTypes` | string] | Action types required for the current step (e.g., `["sumsub"]`, `["sign"]`)                    |
| `failReason`             | string  | Reason for KYC failure (value exists only when `currentStep` is Rejected)                      |

`**SumsubKycInfo` Structure:

| Field Name              | Type   | Description                                                                    |
| ----------------------- | ------ | ------------------------------------------------------------------------------ |
| `userId`                | string | SumSub Applicant ID, used for querying KYC status                              |
| `latestKycHasCompleted` | bool   | Whether the latest KYC process has been completed                              |
| `reviewStatus`          | string | Review status ("init", "pending", "prechecked", "completed", etc.)             |
| `reviewAnswer`          | string | Review result ("GREEN"=Passed, "RED"=Rejected)                                 |
| `reviewRejectType`      | string | Rejection type (e.g., "RETRY", "FINAL", etc.; value exists only when rejected) |

`**CrsInfo` Structure:

| Field Name     | Type   | Description                                                                                                                                                                     |
| -------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `needCrs`      | bool   | Indicates whether the user is required to complete the CRS process.                                                                                                             |
| `restrictDate` | int64  | Represents the deadline or restriction timestamp for completing the CRS process, usually in Unix timestamp format (milliseconds). After this time, the user may be softBlocked. |
| `url`          | string | The CRS submission link that directs the user to the CRS information collection page.                                                                                           |

**Response Field Details (Status):**

* `0 (Na)`: New user, URID NFT not minted.
* `1 (SoftBlocked)`: An ongoing task (CRS, ongoing KYC, or EDD) is overdue; money features are locked until it is completed.
* `2 (Tourist)`: URID NFT minted, KYC not completed.
* `3 (Blocked)`: Account frozen.
* `4 (Closed)`: Account closed.
* `5 (Live)`: KYC passed; all verifications complete, transactions enabled.

**4. Request Example**

```bash
curl -X GET "https://urapi2-qa.ur-inc.xyz/api/v2/account-status" \
-H "tokenId: 12345" \
-H "network: mainnet"

```

**5. Error Codes**

| Code    | Type          | Description                                | Solution                                                  |
| ------- | ------------- | ------------------------------------------ | --------------------------------------------------------- |
| `10001` | Auth Failure  | Invalid `tokenId` or user does not exist   | Check if `tokenId` is correct and user is registered      |
| `10002` | Parse Error   | Missing or malformed Header parameters     | Ensure valid `tokenId` and `network` Headers are provided |
| `10009` | Invalid Param | `tokenId` format error                     | Provide a valid NFT Token ID (positive integer)           |
| `10000` | System Error  | Database query failed or service exception | Retry later or contact technical support                  |

#### 2.1.4 MINT (create user's URID)

| Item            | Value          | Note                                              |
| --------------- | -------------- | ------------------------------------------------- |
| **HTTP Method** | `POST`         |                                                   |
| **URI**         | `/api/v1/mint` |                                                   |
| **Auth Level**  | **Basic Auth** | User status must be `Na` (0)                      |
| **Headers**     | `Content-Type` | Fixed: `application/json`                         |
|                 | `tokenId`      | Pre-generated URID (via generate-token interface) |
|                 | `network`      | Network Identifier (e.g., "mainnet")              |

| Parameter         | Type   | Required | Description                             | Example           |
| ----------------- | ------ | -------- | --------------------------------------- | ----------------- |
| `address`         | string | **Yes**  | User's Ethereum wallet address.         | `"0x123..."`      |
| `lotNumber`       | string | **Yes**  | GeeTest verification session ID.        | `"7a8f..."`       |
| `captchaOutput`   | string | **Yes**  | GeeTest verification output result.     | `"XyZ123..."`     |
| `passToken`       | string | **Yes**  | GeeTest pass token.                     | `"token_..."`     |
| `genTime`         | string | **Yes**  | GeeTest generation timestamp (seconds). | `"1703123456"`    |
| `image`           | string | No       | NFT Image (Base64).                     | `"data:image..."` |
| `backGroundColor` | string | No       | Background color (Hex).                 | `"#FFFFFF"`       |
| `land`            | string | No       | Land information.                       | `"land1"`         |

| Field     | Type   | Description                                      |
| --------- | ------ | ------------------------------------------------ |
| `tokenId` | string | User's URID                                      |
| `txHash`  | string | Blockchain transaction hash for tracking status. |

| Code    | Type           | Description                     | Solution                                         |
| ------- | -------------- | ------------------------------- | ------------------------------------------------ |
| `10002` | Parse Error    | Request parameter format error  | Check JSON format and field types                |
| `10003` | ID Not Found   | Mint Token ID does not exist    | Call `/api/v1/generate-token` first              |
| `10004` | Mismatch       | Token ID does not match address | Confirm TokenID relationship with wallet address |
| `10005` | Duplicate Mint | Address already has an NFT      | Use existing Token ID                            |
| `10007` | Duplicate Mint | Token already used for minting  | Same as 10005                                    |
| `10009` | Invalid Param  | Missing required fields         | Check `address`, `lotNumber`, etc.               |
| `10010` | GeeTest Fail   | Verification failed or expired  | Retry GeeTest verification                       |
| `10006` | Status Error   | Account status is not `Na`      | Check account status                             |

Please refer to [**OpenAPI Mint**](/api-reference#mint-urid) for the URID minting.

#### 2.1.5 Get Sumsub SDK token

**1. Description** Creates an access token for SumSub KYC verification. This token is used to initialize the SumSub SDK (Web or Mobile) and start the identity verification process .

{% hint style="warning" %}
**NFC scanning requires a mobile app.** The Passport/National ID NFC scan step within the Sumsub SDK is only supported in the **Sumsub mobile SDK** (iOS/Android). It is not available in the Sumsub web SDK. Partners must surface this KYC step through their **mobile app**; users on a web browser cannot complete NFC-based identity verification. If your platform is web-only, email <support@ur.app> or use your dedicated integration channel to discuss alternatives.
{% endhint %}

**Call Scenarios:**

* When a user starts the KYC identity verification process.
* When the frontend needs to initialize the SumSub SDK.
* When the token expires during verification and needs refreshing.
* During KYC retry scenarios (supporting various retry levels).

**2. Request**

| Item            | Value                                | Note                      |
| --------------- | ------------------------------------ | ------------------------- |
| **HTTP Method** | `POST`                               |                           |
| **URI**         | `/api/v1/sumsub/create-access-token` |                           |
| **Auth Level**  | **Full Auth**                        |                           |
| **Headers**     | `Content-Type`                       | Fixed: `application/json` |
|                 | `tokenId`                            | User's URID               |
|                 | `network`                            | Network Identifier        |
|                 | `sign`                               | Wallet Signature          |
|                 | `hash`                               | Original request hash     |
|                 | `deadline`                           | Signature deadline        |

**Request Parameters**

| Parameter             | Type   | Required | Description                                                                                                                                                                                                                                 | Example               |
| --------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| `userId`              | string | No       | Optional. When omitted, the server derives the internal Sumsub user id from Full Auth context (e.g. `tokenId`). Only pass this if UR gave you an explicit pattern for your tenant.                                                          | `"12345"`             |
| `levelName`           | string | No       | Specific SumSub Level name. Empty uses default.                                                                                                                                                                                             | `"Basic information"` |
| `ttl`                 | int    | No       | Token time-to-live (seconds). Default configured by server.                                                                                                                                                                                 | `3600`                |
| `isRetryVerification` | bool   | No       | Indicates if this is a KYC retry scenario.                                                                                                                                                                                                  | `false`               |
| `retryLevel`          | int32  | No       | Retry level (`1`-`8`), valid only if `isRetryVerification=true`. `5` and `7` are deprecated; see the **KycRetryVerificationLevel** enum in [OpenAPIs](https://docs.ur.app/api-reference#fetch-ur-account-information) for the full mapping. | `1`                   |
| `stepType`            | string | No       | Specific verification step to reset (e.g., "IDENTITY").                                                                                                                                                                                     | `"IDENTITY"`          |
| `failureReason`       | string | No       | Reason for failure, used for logging.                                                                                                                                                                                                       | `"Document expired"`  |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": "{\"token\":\"act-abc123xyz...\",\"userId\":\"12345\"}",
  "timeNow": 1703123456789
}
```

**Result Field Description**

| Field    | Type   | Description                                                                   |
| -------- | ------ | ----------------------------------------------------------------------------- |
| `token`  | string | SumSub Access Token (usually starts with "act-"). Used to initialize the SDK. |
| `userId` | string | SumSub Applicant ID, used for subsequent queries.                             |

**4. Request Example**

```bash
curl -X POST "https://urapi2-qa.ur-inc.xyz/api/v1/sumsub/create-access-token" \
-H "Content-Type: application/json" \
-H "tokenId: 12345" \
-H "network: mainnet" \
-H "sign: 0x..." \
-H "hash: ..." \
-H "deadline: ..." \
-d '{}'

```

You may include `"userId": "…"` in the JSON body only when UR instructs you to use a tenant-specific pattern.

**5. Error Codes**

| Code    | Type          | Description                        | Solution                                    |
| ------- | ------------- | ---------------------------------- | ------------------------------------------- |
| `10002` | Parse Error   | Request parameter format error     | Check JSON format and optional body fields  |
| `10009` | Invalid Param | Invalid or inconsistent parameters | Verify headers and body per tenant guidance |
| `10000` | System Error  | SumSub service exception           | Retry later or contact support              |

#### 2.1.6 Get user KYC document (Form A)

**1. Description** Retrieves the content and structure of the KYC Form A (Client Due Diligence). This interface is called before the user signs Form A to display the declaration text that requires the user's signature . Form A is a critical KYC document containing compliance information such as financial status, source of funds, and account purpose.

**Call Scenarios:**

* After the user completes SumSub verification, they must fill out/sign Form A.
* The frontend retrieves the specific content to display to the user.
* Retrieves the self-declaration text for the user to confirm via signature.

**2. Request**

| Item            | Value                     | Note                      |
| --------------- | ------------------------- | ------------------------- |
| **HTTP Method** | `GET`                     |                           |
| **URI**         | `/api/v2/kyc/form-a-info` |                           |
| **Auth Level**  | **Full Auth**             |                           |
| **Headers**     | `Content-Type`            | Fixed: `application/json` |
|                 | `tokenId`                 | User's URID               |
|                 | `network`                 | Network Identifier        |
|                 | `sign`                    | Wallet Signature          |
|                 | `hash`                    | Original request hash     |
|                 | `deadline`                | Signature deadline        |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "ok",
  "result": {
    "kycSelfDec": "I declare that I am the beneficial owner of the account..."
  },
  "timeNow": 1678888888
}
```

**Result Field Description**

| Field        | Type   | Description                                                                                                                                                              |
| ------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `kycSelfDec` | string | The complete Form A text content retrieved from the SumSub system. Contains user personal info and compliance declarations. The user must read and sign this exact text. |

**4. Request Example**

```bash
curl -X GET 'https://urapi2-qa.ur-inc.xyz/api/v2/kyc/form-a-info' \
-H "Content-Type: application/json" \
-H "tokenId: 12345" \
-H "network: mainnet" \
-H "sign: 0x..." \
-H "hash: ..." \
-H "deadline: ..." \

```

**5. Error Codes**

| Code    | Type          | Description                                 | Solution                                             |
| ------- | ------------- | ------------------------------------------- | ---------------------------------------------------- |
| `10001` | Auth Failure  | Invalid `tokenId` or user does not exist    | Check if `tokenId` is correct and user is registered |
| `10002` | Parse Error   | Header parameters missing or malformed      | Ensure valid `tokenId` and `network` Headers         |
| `10009` | Invalid Param | `tokenId` format error                      | Provide valid NFT Token ID                           |
| `10000` | System Error  | KYC Flow status incorrect or data exception | Check detailed error message                         |

#### 2.1.7 Submit user signature (submit Form A)

**1. Description** Submits the user's signature for the KYC Form A (Customer Due Diligence Form). After reading the Form A content, the user uses their wallet to sign the content and submits the signature via this interface to complete the final step of the KYC process.

**Call Scenarios:**

* User has retrieved Form A content via `/api/v2/kyc/form-a-info`.
* User reads and confirms the Form A content.
* User uses the wallet to sign the Form A content.
* Submit the signature to complete KYC certification .

**2. Request**

| Item            | Value                       | Note                       |
| --------------- | --------------------------- | -------------------------- |
| **HTTP Method** | `POST`                      |                            |
| **URI**         | `/api/v2/kyc/submit-form-a` |                            |
| **Auth Level**  | **Full Auth**               |                            |
| **Header**      | `Content-Type`              | Fixed: `application/json`  |
| **Header**      | `tokenId`                   | User's URID                |
| **Header**      | `network`                   | Network Identifier: `5000` |
| **Header**      | `sign`                      | Wallet Signature           |
| **Header**      | `hash`                      | Original request hash      |
| **Header**      | `deadline`                  | Signature deadline         |

**Request Parameters**

| Parameter        | Type   | Required | Description                                                                                                                           | Example                            |
| ---------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| `kycSelfDec`     | string | **Yes**  | Form A self-declaration text content. **Must be exactly the same** as the `kycSelfDec` field returned by the `form-a-info` interface. | `"I, John Doe, hereby declare..."` |
| `kycSelfDecSign` | string | **Yes**  | ECDSA signature of the Form A text by the user. (65-byte hex string).                                                                 | `"0x1234...abcd"`                  |

**Parameter Notes:**

* **kycSelfDec:** Must include the complete Form A text content. Cannot be modified or truncated.
* **kycSelfDecSign:**
* Uses the user wallet's private key to sign `kycSelfDec`.
* Format: 65-byte hexadecimal string (130 chars, starting with `0x`).
* Structure: `r` (32 bytes) + `s` (32 bytes) + `v` (1 byte).
* Adopts standard **EIP-191 (Ethereum Signed Message)** mechanism.
* **Verification Logic:** The backend will prefix the `kycSelfDec` text with `\x19Ethereum Signed Message:\n<length>`, calculate the Keccak256 hash, recover the public key address from the signature, and compare it with the current user's wallet address . **Signature Generation Example (Frontend/JS):**

```javascript
// 1. Prepare text (Assume fetched from GET /api/v2/kyc/form-a-info)
const kycSelfDec = "I hereby declare that I am the beneficial owner...";
const userAddress = "0xYourWalletAddress...";

// 2. Sign using Ethers.js
// wallet is a Signer object connected to a Provider
const signature = await wallet.signMessage(kycSelfDec);

// OR 3. Sign using Web3.js
// const signature = await web3.eth.personal.sign(kycSelfDec, userAddress, "password(optional)");

console.log("kycSelfDecSign:", signature);
```

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": "{}",
  "timeNow": 1703123456789
}
```

**Response Description:**

* `retCode`: `0` indicates signature verification passed and saved successfully.
* `result`: Empty JSON object `{}`.
* **Note:** After returning success, wait for **3 seconds** to allow downstream services to process the status update (interface has internal sleep) .

**4. Request Example**

```bash
curl -X POST "https://urapi2-qa.ur-inc.xyz/api/v2/kyc/submit-form-a" \
-H "Content-Type: application/json" \
-H "tokenId: 12345" \
-H "network: mainnet" \
-H "sign: 0x..." \
-H "hash: ..." \
-H "deadline: ..." \
-d '{
  "kycSelfDec": "I, John Doe, holder of passport number XX123456, hereby declare that:\n\n1. The source of funds...",
  "kycSelfDecSign": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
}'

```

**5. Error Codes**

| Code    | Error Type    | Description                                                      | Solution                                                                     |
| ------- | ------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `10001` | Auth Failure  | Invalid `tokenId` or user does not exist.                        | Check if `tokenId` is correct.                                               |
| `10002` | Parse Error   | JSON format error or parameter type mismatch.                    | Ensure `kycSelfDec` and `kycSelfDecSign` are both strings and JSON is valid. |
| `10009` | Invalid Param | Required fields missing or empty.                                | Ensure both `kycSelfDec` and `kycSelfDecSign` are provided.                  |
| `10000` | System Error  | Signature verification failed, database error, or Kafka failure. | Check error message details (e.g., signature mismatch).                      |

### 2.2 Account webhook

Webhook definitions are centralized in [Webhooks](https://docs.ur.app/developer-resources/webhook). For account-related events, see `sumsub_kyc_result` and `kyc_status`.

## 3. Card

This section covers API endpoints related to UR debit card management and banking operations.

### 3.1 Card API

#### 3.1.1 Get user profile

**1. Description** Retrieves the user's banking profile information, including IBAN, account holder name, supported currencies, billing address, and card eligibility status.

**Call Scenarios:**

* Displaying account details and transaction limits to the user.
* Checking if the user has already issued a card or is eligible to do so.

**2. Request**

| Item            | Value          | Note                              |
| --------------- | -------------- | --------------------------------- |
| **HTTP Method** | `GET`          |                                   |
| **URI**         | `/api/v2/br`   |                                   |
| **Auth Level**  | **Full Auth**  | Requires wallet signature         |
| **Headers**     | `Content-Type` | Fixed: `application/json`         |
|                 | `tokenId`      | User's URID                       |
|                 | `network`      | Network Identifier (e.g., `5000`) |
|                 | `sign`         | Wallet Signature                  |
|                 | `hash`         | Original request hash             |
|                 | `deadline`     | Signature deadline                |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "ok",
  "result": {
    "tokenId": 12345,
    "br": "John Doe",
    "iban": "CH93 0076 2011 6238 5295 7",
    "email": "john@example.com",
    "mobile": "+41791234567",
    "debitCard": "MSTD",
    "isCardEligible": true,
    "cards": [],
    "cardActivation": {
      "amount": 100,
      "currency": "CHF"
    },
    "street": "Bahnhofstrasse 1",
    "postalCode": "8001",
    "city": "Zurich",
    "country": "CHE",
    "limits": {
      "restartDate": "2024-02-01",
      "restartDateMs": 1706745600000,
      "used": 500,
      "available": 9500,
      "max": 10000
    },
    "contacts": {
      "CHF": [],
      "EUR": [
        {
          "id": "EA-00082721",
          "name": "Mark Lee",
          "account": "•••• 8271",
          "fullAccount": "CH26 9323 0923 1234 9876 8",
          "bank": "SR Saphirstein AG",
          "isSameOwner": true,
          "isIBAN": true,
          "country": "CH",
          "lastPaymentDate": 1771459200000
        }
      ],
      "USD": []
    },
    "depositBank": {
      "CHF": {
        "account": "CH93 0076 ...",
        "bank": "Hypothekarbank Lenzburg AG",
        "BIC": "HYPCH22",
        "payee": "Fiat24 AG",
        "city": "Lenzburg"
      }
    }
  },
  "timeNow": 1737460800000
}
```

**Result Field Description**

| Field            | Type    | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ---------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `br`             | string  | Account Holder Name.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `iban`           | string  | User's IBAN (formatted with spaces).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `country`        | string  | ISO3 Country Code (e.g., "CHE" for Switzerland).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `isCardEligible` | boolean | Indicates if the user is eligible to issue a card.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `cards`          | array   | List of existing cards.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `cardActivation` | object  | Activation fee info (Amount/Currency). Returned only if `isCardEligible=true` and `cards` is empty.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `limits`         | object  | The `limits` object defines the user's monthly spending capacity, denominated in CHF (Swiss Franc), within a rolling 30-day cycle.It consists of `max`, representing the total allowable limit allocated to the user , and `used`, which tracks the cumulative volume of all fiat-related operations (including FX, Card Spending, On-ramps, and Cash Payouts). The `available` balance is dynamically calculated as the difference between `max` and `used`.To ensure transaction success, partners must verify that any requested outgoing transaction amount does not exceed the `available` limit, as outgoing operations surpassing this threshold will automatically fail. |
| `depositBank`    | object  | Bank account details for topping up the account (grouped by currency). EUR and CHF use the default IBAN, issued automatically when the user reaches `Live`. USD uses a separate USD IBAN, issued only after you request it with `POST /v1/apply-usd-payin` (a synchronous call; UR creates the IBAN immediately if the user is `Live`).                                                                                                                                                                                                                                                                                                                                          |
| contacts         | object  | The user's bank payout contact list.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |

**4. Request Example**

```bash
curl -X GET 'https://urapi3-qa.ur-inc.xyz/api/v2/br' \
-H 'tokenid: 12345' \
-H 'network: 5000' \
-H 'sign: 0x1234567890abcdef...' \
-H 'hash: 0xabcdef1234567890...' \
-H 'deadline: 1737500000'

```

**5. Error Codes**

| Code    | Type         | Description                   | Solution                                                        |
| ------- | ------------ | ----------------------------- | --------------------------------------------------------------- |
| `10000` | DefError     | Default/Upstream Error        | Check request parameters and system status.                     |
| `10001` | Auth Failure | Signature Verification Failed | Check headers, signature, deadline validity, and address match. |
| `10002` | Parse Error  | Request Parsing Failed        | Validate JSON format and Content-Type.                          |
| `10006` | Status Error | Account Status Not Supported  | User account must be in a valid state (e.g., Live).             |
| `10016` | Rate Limit   | Too Many Requests             | Retry later (exponential backoff).                              |
| `10999` | Blocked      | Trade Blocked                 | Account restricted; contact support.                            |

#### 3.1.2 Create card

**1. Description:** After passing KYC verification and meeting card issuance conditions, the user calls this interface to apply for a new virtual card. This process typically involves an initial top-up amount and currency specification.

**2. Request**

| Item            | Value          | Note                                     |
| --------------- | -------------- | ---------------------------------------- |
| **HTTP Method** | `POST`         |                                          |
| **URI**         | `/api/v2/card` |                                          |
| **Auth Level**  | **Full Auth**  | Requires wallet signature                |
| **Headers**     | `Content-Type` | Fixed: `application/json`                |
|                 | `tokenId`      | User's URID                              |
|                 | `network`      | Network Identifier (e.g., `5000`/`5003`) |
|                 | `sign`         | Wallet Signature                         |
|                 | `hash`         | Original request hash                    |
|                 | `deadline`     | Signature deadline                       |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "",
  "result": {
    "status": 200,
    "data": "Card created correctly"
  },
  "timeNow": 1700000000
}
```

**4. Request Example**

```bash
curl -X POST 'https://urapi3-qa.ur-inc.xyz/api/v2/card' \
-H 'Content-Type: application/json' \
-H 'tokenid: 1001' \
-H 'network: 5000' \
-H 'sign: 0x5a2...3b1' \
-H 'hash: Hello world' \
-H 'deadline: 1735689600' \
-d '{}'

```

**5. Error Codes**

| Code    | Type     | Description                                           | Solution                                                                                               |
| ------- | -------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `10000` | DefError | Default error containing specific business error info | Return 400 (Parameter error or conditions not met), Unauthorized signature, or Service Internal Error. |

#### 3.1.3 Get card info

**1. Description** Retrieves card information and the `cardToken`. The `cardToken` is used to initialize the frontend component to securely display sensitive card details such as the CVV.

**2. Request**

| Item            | Value          | Note                               |
| --------------- | -------------- | ---------------------------------- |
| **HTTP Method** | `GET`          |                                    |
| **URI**         | `/api/v2/card` |                                    |
| **Auth Level**  | **Full Auth**  | Requires wallet signature          |
| **Headers**     | `Content-Type` | Fixed: `application/json`          |
|                 | `tokenId`      | User's URID                        |
|                 | `network`      | Network Identifier (`5000`/`5003`) |
|                 | `sign`         | Wallet Signature                   |
|                 | `hash`         | Original request hash              |
|                 | `deadline`     | Signature deadline                 |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "",
  "result": {
    "security": {
      "contactlessEnabled": true,
      "withdrawalEnabled": false,
      "internetPurchaseEnabled": true,
      "overallLimitsEnabled": true
    },
    "currencies": ["EUR", "CHF", "USD", "RMB"],
    "tokenId": 106654866313,
    "limits": {
      "account": {
        "restartDate": "01.02.2026 9:47",
        "restartDateMs": 1769939264000,
        "used": 33645.39,
        "available": 760005.39,
        "max": 793650.79
      },
      "withdrawal": { "used": 0, "max": 0 },
      "internetPurchase": { "used": 4858.63, "max": 165010 }
    },
    "cardDesign": "MSTDMNT",
    "cardHolder": "Shawn XX",
    "status": "Active",
    "currency": "CNH",
    "masked": {
      "cardNumber": ".... 3083",
      "cvv2": "...",
      "expiry": "../.."
    },
    "cardToken": "eyJ0b2tlbiI6IjQ1Y2IwMjU5LTk2ZjgtNGMzMi1hZDIzLWEwNWZmYzkxOWI5YSZ...",
    "activeTokens": [
      {
        "id": "704ab18a...",
        "type": "iPhone 16 pro (Apple Pay)",
        "createdAt": "2026-01-06T16:40:26Z"
      }
    ],
    "externalId": "1758893252"
  },
  "timeNow": 1769440233208
}
```

**4. Request Example**

```bash
curl -X GET 'https://urapi3-qa.ur-inc.xyz/api/v2/card' \
-H 'accept: application/json' \
-H 'tokenid: 6654866313' \
-H 'network: 5000' \
-H 'sign: 0xe6be0e52...' \
-H 'hash: UR' \
-H 'deadline: 1769410673'

```

**5. Error Codes**

| Code    | Type     | Description                                            | Reason                                                                                                                                                                             |
| ------- | -------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `10000` | DefError | Default error containing specific business error info. | 1. Request parameters error (unsupported currency, card not found/not created). 2. Unauthorized: Signature verification failed. 3. Service Internal Error. 4. Data parsing failed. |

#### 3.1.4 Get card details

The card API does not expose sensitive debit card data such as PAN, CVV, or expiry in JSON. Use `cardToken` only to render those sensitive fields through UR's card display script. The `cardToken` is short-lived and expires after 5 minutes. When it expires, call [`GET /api/v2/card`](#id-3.1.3-get-card-info) again to get a fresh token.

Card identifiers:

| Field               | Use                                                                                                    |
| ------------------- | ------------------------------------------------------------------------------------------------------ |
| `cardToken`         | Short-lived token for card detail display only. Do not store or log it.                                |
| `externalId`        | Stable card management ID. Use it for APIs such as Set Default Transaction Currency.                   |
| `activeTokens[].id` | Device wallet token ID, such as Apple Pay. Do not use it for card detail display or currency settings. |

Load the script from UR:

```html
<script src="https://openapi.ur.app/api/v1/card-display/card.js"></script>
```

Add DOM placeholders where the script should render sensitive fields:

```html
<div class="card-details">
  <div class="card-number-row">
    <div id="cardNumbers"></div>
    <button type="button" id="cardNumbersCopy" aria-label="Copy card number"></button>
  </div>
  <div class="card-meta-row">
    <span id="cardExpiryDate"></span>
    <span id="cardCvvDate"></span>
  </div>
</div>
```

Initialize the display after the user chooses to reveal card details:

```js
const mobile = window.matchMedia("(max-width: 640px)").matches;
const cardTextStyle = {
  background: "transparent",
  color: "#000",
  "font-size": mobile ? "1em" : "23px",
  "font-family": "\"Helvetica Neue\", Helvetica, Arial, sans-serif",
  "letter-spacing": "2px",
  "font-weight": "500"
};

window.fiat24card.bootstrap({
  clientAccessToken: cardToken,
  component: {
    showPan: {
      cardPan: {
        domId: "cardNumbers",
        format: true,
        styles: { span: cardTextStyle }
      },
      copyCardPan: {
        domId: "cardNumbersCopy",
        mode: "transparent",
        onCopySuccess: () => console.log("Card number copied"),
        onCopyFailure: error => console.error("Unable to copy card number", error)
      },
      cardExp: {
        domId: "cardExpiryDate",
        format: true,
        styles: { span: cardTextStyle }
      },
      cardCvv: {
        domId: "cardCvvDate",
        styles: { span: cardTextStyle }
      }
    }
  },
  callbackEvents: {
    onSuccess: () => console.log("Card details rendered"),
    onFailure: error => console.error("Unable to render card details", error)
  }
});
```

Common mistakes:

* Do not store or log `cardToken`.
* Do not use `activeTokens[].id` unless calling a device-token management API.
* Load the script only on the card details view or secure webview, not globally across your app.
* Render card details only after explicit user action, such as selecting "Show card details".

#### 3.1.5 Set default transaction currency

**1. Description** Sets the default transaction currency for the user's card.

**2. Request**

| Item            | Value                   | Note                               |
| --------------- | ----------------------- | ---------------------------------- |
| **HTTP Method** | `POST`                  |                                    |
| **URI**         | `/api/v2/card-currency` |                                    |
| **Auth Level**  | **Full Auth**           |                                    |
| **Headers**     | `Content-Type`          | Fixed: `application/json`          |
|                 | `tokenId`               | User's URID                        |
|                 | `network`               | Network Identifier (`5000`/`5003`) |
|                 | `sign`                  | Wallet Signature                   |
|                 | `hash`                  | Original request hash              |
|                 | `deadline`              | Signature deadline                 |

**Request Parameters**

| Parameter        | Type   | Description                                                                    |
| ---------------- | ------ | ------------------------------------------------------------------------------ |
| `cardExternalId` | string | Stable card external ID returned by `GET /api/v2/card` as `result.externalId`. |
| `currency`       | string | The default transaction currency to set, such as `USD`, `EUR`, or `CHF`.       |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "",
  "result": {
    "status": 200,
    "data": "currency"
  },
  "timeNow": 1700000000
}
```

**4. Request Example**

```bash
curl -X POST 'https://urapi3-qa.ur-inc.xyz/api/v2/card-currency' \
-H 'Content-Type: application/json' \
-H 'tokenid: 1001' \
-H 'network: 5000' \
-H 'sign: 0x5a2...3b1' \
-H 'hash: Hello world' \
-H 'deadline: 1735689600' \
-d '{
  "cardExternalId": "1758893252",
  "currency": "USD"
}'

```

Notes:

* Use the stable `externalId` value returned by `GET /api/v2/card`.

**5. Error Codes**

| Code    | Type     | Description                                            | Solution                                                                                                                           |
| ------- | -------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `10000` | DefError | Default error containing specific business error info. | 1. Parameter errors (e.g., unsupported currency, card does not exist), 2. Signature verification failed 3. Upstream service error. |

#### 3.1.6 Get transaction history

**1. Description** Retrieves the transaction history for the user's account. Supports date range filtering and pagination.

**2. Request**

| Item            | Value                  | Note                              |
| --------------- | ---------------------- | --------------------------------- |
| **HTTP Method** | `POST`                 |                                   |
| **URI**         | `/api/v2/transactions` |                                   |
| **Auth Level**  | **Full Auth**          | Requires wallet signature         |
| **Headers**     | `Content-Type`         | Fixed: `application/json`         |
|                 | `tokenId`              | User's URID                       |
|                 | `network`              | Network Identifier (e.g., `5000`) |
|                 | `sign`                 | Wallet Signature                  |
|                 | `hash`                 | Original request hash             |
|                 | `deadline`             | Signature deadline                |

**Request Body**

| Parameter                      | Type          | Required | Description                                                                                                   |
| ------------------------------ | ------------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| `pageSize`                     | int           | No       | Page size (default 50).                                                                                       |
| `fromTimestamp`                | long          | No       | Start timestamp (**Unix seconds**).                                                                           |
| `toTimestamp`                  | long          | No       | End timestamp (**Unix seconds**).                                                                             |
| `type`                         | string        | No       | Single transaction type (e.g. `P2P`, `FRX`, `CTU`, `CRD`, `CDP`). Mutually exclusive with `transactionTypes`. |
| `transactionTypes`             | string\[]     | No       | Multiple transaction-type filter. Mutually exclusive with `type`.                                             |
| `currencys`                    | string\[]     | No       | Currency filter (e.g. `EUR`, `USD`, `CHF`, `CNH`).                                                            |
| `direction`                    | string        | No       | `IN`, `OUT`, or `ALL`.                                                                                        |
| `minAmount` / `maxAmount`      | string        | No       | Amount range filter.                                                                                          |
| `status`                       | string        | No       | Transaction status filter.                                                                                    |
| `chainId`                      | string        | No       | e.g. `eip155:5000`.                                                                                           |
| `tokenSymbol`                  | string        | No       | e.g. `USDC`.                                                                                                  |
| `cursorTimestamp` / `cursorId` | long          | No       | Forward-pagination cursor.                                                                                    |
| `id` / `txHash`                | long / string | No       | Query a single transaction (mutually exclusive).                                                              |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "ok",
  "result": {
    "transactions": [
      {
        "id": 42,
        "title": "Card spend",
        "subtitle": "Starbucks",
        "amount": "-15.50",
        "type": "CRD",
        "timestamp": 1698402600,
        "image": "",
        "currency": "USD",
        "direction": "OUT",
        "txHash": "0xdead…beef",
        "chainId": "eip155:5000",
        "mcc": 5812
      },
      {
        "id": 41,
        "title": "Crypto top-up",
        "subtitle": "eip155:5000 USDC",
        "amount": "+1000.00",
        "type": "CTU",
        "timestamp": 1698310800,
        "currency": "EUR",
        "direction": "IN",
        "txHash": "0xabc…123",
        "chainId": "eip155:5000",
        "inputToken": "USDC",
        "inputAmount": "1000000000"
      }
    ]
  },
  "timeNow": 1698489000000
}
```

**Result Field Description** (each entry is a `TransactionV2`; fields are shared with `TransactionData`; see [Delegated Contract Mode → TransactionData](/api-reference/account/delegated-contract-mode))

| Field                                            | Type   | Description                                                                                 |
| ------------------------------------------------ | ------ | ------------------------------------------------------------------------------------------- |
| `id`                                             | long   | Transaction record id.                                                                      |
| `title`, `subtitle`                              | string | Display strings.                                                                            |
| `amount`                                         | string | Signed decimal string (e.g. `"-15.50"`, `"+1000.00"`).                                      |
| `type`                                           | string | Transaction type: `P2P`, `FRX`, `CTU`, `CRD`, `CDP`, `CWD`, `CTF`, `CSW`, `ONR`, `UNKNOWN`. |
| `timestamp`                                      | long   | **Unix seconds.**                                                                           |
| `currency`                                       | string | Transaction currency (e.g. `USD`, `EUR`).                                                   |
| `direction`                                      | string | `IN` or `OUT`.                                                                              |
| `txHash`, `chainId`                              | string | On-chain identifiers; `chainId` is CAIP-2.                                                  |
| `inputToken`, `inputAmount`, `inputTokenAddress` | string | Source asset (CTU).                                                                         |
| `outputAmount`                                   | string | FX output (FRX).                                                                            |
| `mcc`                                            | uint64 | Merchant category code (CRD).                                                               |
| `reference`, `fee`, `transferredAmount`, `notes` | string | Channel-specific extras (CWD/CDP/CTU/ONR).                                                  |

**4. Request Example**

```bash
curl -X POST 'https://urapi3-qa.ur-inc.xyz/api/v2/transactions' \
-H 'Content-Type: application/json' \
-H 'tokenId: 12345' \
-H 'network: 5000' \
-H 'sign: 0x123...' \
-H 'hash: HistoryQuery' \
-H 'deadline: 1735689600' \
-d '{
  "pageSize": 50,
  "fromTimestamp": 1698300000,
  "toTimestamp": 1698500000
}'

```

**5. Error Codes**

| Code    | Type         | Description                                            | Solution                                             |
| ------- | ------------ | ------------------------------------------------------ | ---------------------------------------------------- |
| `10000` | DefError     | Default error containing specific business error info. | Check request parameters or upstream service status. |
| `10001` | Auth Failure | Signature Verification Failed                          | Check headers, signature, and deadline validity.     |

#### 3.1.7 Update card status

**1. Description** Modifies the status of the user's debit card. This is primarily used to "Freeze" (Block) the card to prevent unauthorized usage or "Unfreeze" (Unblock) it to resume normal operations.

**2. Request**

| Item            | Value                 | Note                              |
| --------------- | --------------------- | --------------------------------- |
| **HTTP Method** | `POST`                |                                   |
| **URI**         | `/api/v2/card-status` |                                   |
| **Auth Level**  | **Full Auth**         | Requires wallet signature         |
| **Headers**     | `Content-Type`        | Fixed: `application/json`         |
|                 | `tokenId`             | User's URID                       |
|                 | `network`             | Network Identifier (e.g., `5000`) |
|                 | `sign`                | Wallet Signature                  |
|                 | `hash`                | Original request hash             |
|                 | `deadline`            | Signature deadline                |

**Request Parameters**

| Parameter     | Type   | Required | Description                                                                                                                                           | Example  |
| ------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `cardTokenId` | string | **Yes**  | The Card's Token (Note: This is the card's specific ID, not the user's URID). The value is 'cardToken', can be retrieved from the `/cards` interface. | `"1001"` |
| `status`      | int    | **Yes**  | Target status code. `0`: Inactive (Blocked/Frozen) `1`: Active (Unblocked)                                                                            | `0`      |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "status": 200,
    "data": "Status updated"
  },
  "timeNow": 1700000000
}
```

**4. Request Example**

```bash
curl -X POST 'https://urapi3-qa.ur-inc.xyz/api/v2/card-status' \
-H 'Content-Type: application/json' \
-H 'tokenid: 12345' \
-H 'network: 5000' \
-H 'sign: 0xabcdef...' \
-H 'hash: UpdateStatus' \
-H 'deadline: 1735689600' \
-d '{
  "cardTokenId": "1001",
  "status": 0
}'

```

**5. Error Codes**

| Code    | Type         | Description                                            | Solution                                                        |
| ------- | ------------ | ------------------------------------------------------ | --------------------------------------------------------------- |
| `10000` | DefError     | Default error containing specific business error info. | Card not found, invalid status code, or upstream service error. |
| `10001` | Auth Failure | Signature Verification Failed                          | Check signature and headers.                                    |

#### 3.1.8 Permit (token approval)

**1. Description** Submits an EIP-2612 compatible permit signature. This allows the UR contracts to spend tokens from the user's wallet (When spending with the card, the UR contract will deduct the fiat balance from the user's UR account.) without the user needing to execute an on-chain `approve` transaction.

**2. Request**

| Item            | Value                  | Note                                |
| --------------- | ---------------------- | ----------------------------------- |
| **HTTP Method** | `POST`                 |                                     |
| **URI**         | `/api/v1/token-permit` |                                     |
| **Auth Level**  | **Full Auth**          | Requires API-level wallet signature |
| **Headers**     | `Content-Type`         | Fixed: `application/json`           |
|                 | `tokenId`              | User's URID                         |
|                 | `network`              | Network Identifier (e.g., `5000`)   |
|                 | `sign`                 | Signature and Verify Signature      |
|                 | `hash`                 | Original request hash               |
|                 | `deadline`             | API Signature deadline              |

**Request Parameters**

| Parameter        | Type   | Required | Description                                                          | Example      |
| ---------------- | ------ | -------- | -------------------------------------------------------------------- | ------------ |
| `address`        | string | **Yes**  | The contract address of the token being approved (e.g., USDC, USDT). | `"0x..."`    |
| `amount`         | string | **Yes**  | The amount to approve (decimal string).                              | `"100"`      |
| `permitAmount`   | string | **Yes**  | The permit amount signed in the EIP-2612 permit (decimal string).    | `"100"`      |
| `permitDeadline` | int    | **Yes**  | The Unix timestamp (seconds) until which the permit is valid.        | `1735689600` |
| `permitV`        | int    | **Yes**  | EIP-2612 permit signature component `v`.                             | `28`         |
| `permitR`        | string | **Yes**  | EIP-2612 permit signature component `r` (32 bytes hex).              | `"0x..."`    |
| `permitS`        | string | **Yes**  | EIP-2612 permit signature component `s` (32 bytes hex).              | `"0x..."`    |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "status": 200,
    "txHash": "0xabc123..."
  },
  "timeNow": 1700000000
}
```

**Result Field Description**

| Field    | Type   | Description                                                                                                |
| -------- | ------ | ---------------------------------------------------------------------------------------------------------- |
| `txHash` | string | The transaction hash of the permit execution (if the server relays it immediately) or status confirmation. |

**4. Request Example**

```bash
curl -X POST 'https://urapi3-qa.ur-inc.xyz/api/v1/token-permit' \
-H 'Content-Type: application/json' \
-H 'tokenId: 12345' \
-H 'network: 5000' \
-H 'sign: 0xApiSign...' \
-H 'hash: PermitReq' \
-H 'deadline: 1735689600' \
-d '{
  "address": "0xTokenAddr...",
  "amount": "100",
  "permitAmount": "100",
  "permitDeadline": 1740000000,
  "permitV": 28,
  "permitR": "0x...",
  "permitS": "0x..."
}'

```

You can find the token addresses [at here](/api-reference/smart-contracts#contract-addresses-1).

**5. Error Codes**

| Code    | Type          | Description                       | Solution                                                          |
| ------- | ------------- | --------------------------------- | ----------------------------------------------------------------- |
| `10000` | System Error  | Execution failed                  | Check if the deadline has expired or if the signature is invalid. |
| `10001` | Auth Failure  | API Signature Verification Failed | Check headers and API-level authentication.                       |
| `10009` | Invalid Param | Malformed signature parameters    | Ensure `v`, `r`, `s` are correctly formatted.                     |

#### 3.1.9 Get supported chain config

**1. Description** Queries the chain configuration information supported by the UR application, including the list of tokens on each chain, user balances, card issuance eligibility status, and Token activity configurations.

**Note:** This interface supports queries in both **logged-in** and **non-logged-in** states.

* **Non-logged-in:** Only queries supported chains and contract addresses.
* **Logged-in:** Additionally returns the user's asset information.

**Call Scenarios:**

* Getting supported chains and token lists when the App starts.
* Refreshing balances when the user enters the asset page.
* Querying token info on the target chain before top-up.
* Querying supported chains and contract addresses before withdrawal .

**2. Request**

| Item            | Value                          | Note                       |
| --------------- | ------------------------------ | -------------------------- |
| **HTTP Method** | `GET`                          |                            |
| **URI**         | `/api/v3/config/chain-configs` |                            |
| **Auth Level**  | **Full Auth**                  |                            |
| **Headers**     | `Content-Type`                 | Fixed: `application/json`  |
|                 | `tokenId`                      | User's URID                |
|                 | `network`                      | Network Identifier: `5000` |
|                 | `sign`                         | Wallet Signature           |
|                 | `hash`                         | Original request hash      |
|                 | `deadline`                     | Signature deadline         |

**3. Response**

**Success Response (HTTP 200):**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "chains": [
      {
        "chainIdentifier": "eip155:5000",
        "chainName": "Mantle",
        "chainLogoUrl": "https://example.com/mantle.png",
        "tokens": [
          {
            "tokenIdentifier": "0x09Bc1633B9f7B10517C089d825C8C3D9AA153103",
            "symbol": "USDC",
            "name": "USD Coin",
            "logoUrl": "https://example.com/usdc.png",
            "decimals": 6,
            "displayDecimals": 2,
            "isFiat": false,
            "isNative": false,
            "canDeposit": true,
            "canWithdraw": true,
            "aggregatorSupported": true,
            "priority": 1,
            "minFxAmount": "10",
            "maxFxAmount": "1000000",
            "minTopUpAmount": "10",
            "maxTopUpAmount": "5000000"
          },
          {
            "tokenIdentifier": "0x0000000000000000000000000000000000000000",
            "symbol": "MNT",
            "name": "Mantle",
            "isNative": true,
            "canDeposit": false,
            "canWithdraw": false
          }
        ],
        "nativeToken": {
          "symbol": "MNT",
          "isNative": true
        },
        "depositContract": "0x8C922114d626305E8ebC38559f17A84b31f31f17",
        "eip7702DelegationContract": "0x7A5e0CaE6F66d6D0E2b4060347C07B9d3D3Fb9e6",
        "bufferPoolContract": "0x1234567890abcdef1234567890abcdef12345678"
      },
      {
        "chainIdentifier": "eip155:1",
        "chainName": "Ethereum",
        "tokens": [
          {
            "tokenIdentifier": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
            "symbol": "USDC",
            "decimals": 6,
            "balance": "500.00",
            "balanceUsd": "500.00",
            "canDeposit": true,
            "canWithdraw": true
          }
        ],
        "depositContract": "0x..."
      }
    ],
    "canActivateCard": true,
    "cardActivationMessage": "You are eligible to apply for a card!",
    "tokenActivities": [
      {
        "symbol": "USDe",
        "title": "Earn 15.4% APY",
        "targetUrl": "https://ur.app/earn/usde",
        "targetType": "webview"
      }
    ]
  },
  "timeNow": 1716259400000
}
```

**Result Field Breakdown:**

**Main Response Structure:**

| Field Name              | Type    | Description                               |
| ----------------------- | ------- | ----------------------------------------- |
| `chains`                | array   | List of supported chain configurations    |
| `canActivateCard`       | boolean | Whether the card can be activated         |
| `cardActivationMessage` | string  | Message prompt for card activation status |
| `tokenActivities`       | array   | List of Token activity configurations     |

`**chains` Array Element Structure (ChainInfo):

| Field Name                  | Type   | Description                                               |
| --------------------------- | ------ | --------------------------------------------------------- |
| `chainIdentifier`           | string | Unique chain identifier (CAIP-2 format, e.g., `eip155:1`) |
| `chainName`                 | string | Human-readable name of the chain                          |
| `chainLogoUrl`              | string | URL of the chain Logo                                     |
| `tokens`                    | array  | List of tokens supported on this chain                    |
| `nativeToken`               | object | Native token information                                  |
| `depositContract`           | string | Deposit contract address                                  |
| `eip7702DelegationContract` | string | EIP-7702 Delegation contract address                      |
| `bufferPoolContract`        | string | Buffer Pool contract address (for Onramp)                 |

`**tokens` Array Element Structure (TokenInfo):

| Field Name            | Type    | Description                                                                                                        |
| --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
| `tokenIdentifier`     | string  | Unique token identifier (contract address or special value)                                                        |
| `symbol`              | string  | Token symbol (USDC, ETH, etc.)                                                                                     |
| `name`                | string  | Full name of the token                                                                                             |
| `logoUrl`             | string  | URL of the token Logo                                                                                              |
| `decimals`            | int     | Token decimals                                                                                                     |
| `displayDecimals`     | int     | Frontend display decimals                                                                                          |
| `isFiat`              | boolean | Whether it is a fiat token                                                                                         |
| `totalBalance`        | string  | User balance (Decimal String)                                                                                      |
| `totalBalanceUsd`     | string  | USD value of the balance                                                                                           |
| `isNative`            | boolean | Whether it is a native token                                                                                       |
| `canDeposit`          | boolean | Whether deposit is supported                                                                                       |
| `canWithdraw`         | boolean | Whether withdrawal is supported                                                                                    |
| `aggregatorSupported` | boolean | Whether DEX aggregator is supported                                                                                |
| `priority`            | int     | Sorting priority                                                                                                   |
| `minTopUpAmount`      | string  | Minimum Off-ramp amount for this token, in the token's own units (converted from a USD anchor at the live rate).   |
| `maxTopUpAmount`      | string  | Maximum Off-ramp amount for this token, in the token's own units (converted from a USD anchor at the live rate).   |
| `minTopUpAmountUSD`   | string  | USD anchor behind `minTopUpAmount`; identical across tokens.                                                       |
| `maxTopUpAmountUSD`   | string  | USD anchor behind `maxTopUpAmount`; identical across tokens.                                                       |
| `minFxAmount`         | string  | Minimum FX amount for this token, in the token's own units (converted from a USD anchor at the live rate).         |
| `maxFxAmount`         | string  | Maximum FX amount for this token; reflects the user's remaining rolling 30-day allowance, so it changes over time. |
| `minFxAmountUSD`      | string  | USD anchor behind `minFxAmount`; identical across tokens.                                                          |
| `maxFxAmountUSD`      | string  | USD anchor behind `maxFxAmount`.                                                                                   |

**Amount limits: how to read them.** The `minTopUpAmount` and `maxTopUpAmount` fields bound Off-ramp; "top-up" is the chain config's name for an Off-ramp deposit. The `minFxAmount` and `maxFxAmount` fields bound FX. UR derives each limit from a single USD anchor and converts it to the token's own units at the live exchange rate, so the value differs across tokens and moves with rates. Read these fields at request time; do not hardcode them. The `...USD` fields carry the underlying USD anchor, which is the same for every token.

`**tokenActivities` Array Element Structure (TokenActivity):

| Field Name   | Type   | Description                               |
| ------------ | ------ | ----------------------------------------- |
| `symbol`     | string | Associated token symbol                   |
| `title`      | string | Activity title (may contain dynamic APY)  |
| `targetUrl`  | string | Link to jump to on click                  |
| `targetType` | string | Jump type (`webview`/`native`/`external`) |

**Response Field Details:**

* **chains**: List of supported chains.
* Sorted by priority, usually the Mantle chain comes first.
* Each chain contains configuration for tokens, contract addresses, etc.
* `balance` field is not included for non-logged-in users .
* **canActivateCard**: Card issuance eligibility.
* Valid only in logged-in state.
* Logic: Checks if any bank fiat balance on the Mantle chain is ≥ 1 USD.
* `true`: User meets the conditions for card issuance.
* `false`: Does not meet conditions .
* **cardActivationMessage**: Card issuance prompt text.
* Used in conjunction with `canActivateCard`.
* Informs the user of the card issuance status or guides the operation .
* **tokenIdentifier**: Token identifier.
* ERC20 Token: Contract address.
* Native Token: Usually `0x0000000000000000000000000000000000000000` or `"NATIVE"` .
* **CAIP-2 Format**:
* Chain identifiers use the CAIP-2 standard.
* Format: `<blockchainId>:<chainId>`
* Example: `eip155:1` (Ethereum), `eip155:5000` (Mantle) .
* **Decimal String**:
* Amount fields use string format.
* Avoids JavaScript large integer precision issues.
* Example: `"1000.00"`, `"0.000001"` .

**4. Request Example**

**Basic Query - Non-logged-in State:**

```bash
curl -X GET "https://urapi3-qa.ur-inc.xyz/api/v3/config/chain-configs"

```

**Logged-in State - Get Balance and Card Status:**

```bash
curl -X GET "https://urapi3-qa.ur-inc.xyz/api/v3/config/chain-configs" \
-H "tokenId: 12345" \
-H "User-Agent: URBank/3.3.0 (105)"

```

**Full Request Example:**

```bash
curl -X GET "https://urapi3-qa.ur-inc.xyz/api/v3/config/chain-configs" \
-H "User-Agent: URBank/3.3.0 (Android)" \
-H "tokenId: 12345" \
-H "internal-address: 0x742d35Cc6634C0532925a3b844Bc9e7595f4bFA5"

```

**5. Error Codes**

| Error Code | Error Type    | Description                                          | Solution                                        |
| ---------- | ------------- | ---------------------------------------------------- | ----------------------------------------------- |
| `10000`    | System Error  | Database query failed, Nacos config retrieval failed | Retry later or contact technical support        |
| `10001`    | Auth Failure  | Invalid `tokenId` format                             | Check if `tokenId` format is correct            |
| `10009`    | Invalid Param | `address` parameter format error                     | Ensure `address` is a valid hexadecimal address |

**Error Response Examples:**

```json
{
  "retCode": 10000,
  "retMsg": "DefError: database connection failed",
  "result": null,
  "timeNow": 1716259400000
}
```

```json
{
  "retCode": 10009,
  "retMsg": "ParamInvalid: invalid address format",
  "result": null,
  "timeNow": 1716259400000
}
```

**Common Error Troubleshooting:**

**i. Nacos Config Retrieval Failure (10000):**

* Token activities may not show.
* The main flow (chain configuration) is unaffected.
* Check Nacos service status .

**ii. Balance Query Timeout (10000):**

* May be encountered by logged-in users.
* Likely due to slow chain node response.
* Balances for some chains may be missing .

**iii. Address Format Error (10009):**

* Ensure the address starts with `0x`.
* Ensure it is a valid hexadecimal string.
* Address length should be 42 characters (`0x` + 40 hex) .

**Usage Instructions:**

* **Cache Strategy:**
* Recommended cache time: 5-10 minutes.
* Balance data needs frequent refreshing .
* **Version Compatibility:**
* Older versions of the App will automatically filter `USDe`.
* Ensure App version ≥ 3.3.0 to access full features .
* **Chain List Sorting:**
* Sorted by the `priority` field.
* Mantle chain usually ranks first .
* **Token Filtering:**
* App version < 3.3.0 will filter `USDe`.
* Filtering logic can be controlled via `User-Agent` .

## 4. Offramp

This section introduces APIs and webhooks related to Offramp operations. Subsections are organized by chain family (e.g. EVM vs non-EVM); **§4.1** covers EVM-compatible networks, and **§4.2** covers Solana.

**Amount limits.** Read the minimum and maximum Off-ramp amount for each source token from the chain config fields `minTopUpAmount` and `maxTopUpAmount`; see [Get Supported Chain Config](#id-3.1.9-get-supported-chain-config). Read them at request time and do not hardcode them, because UR converts a USD anchor at the live rate for each token. The resulting USDC amount must be at least 5 USDC; UR rejects a smaller amount with error code `20003`. Each Off-ramp also counts against the user's rolling 30-day fiat limit.

### 4.1 Offramp API for EVM

**Note:** The APIs and contract-oriented flows in **§4.1** apply to **EVM-compatible chains** only; for example Ethereum mainnet (L1), Arbitrum One, Base, Mantle, and other supported networks that use CAIP-2 `eip155:*` chain IDs. **Solana** offramp is documented in **§4.2**.

#### 4.1.1 Get quote

**1. Description** Retrieves the best quote for a cross-chain cryptocurrency deposit. The system aggregates multiple DEXs (such as 1inch, Odos, etc.) to find the optimal exchange route, calculates network fees, cross-chain fees, and the final output amount, providing the user with complete deposit quote information.

**Business Scenarios:**

* **Cross-chain Deposit Process:** Users holding tokens on other chains (e.g., Ethereum, Arbitrum) want to deposit into UR.
* **USDC Direct Deposit:** Users holding USDC can deposit directly without exchange.
* **Non-USDC Token Deposit:** Users holding other tokens (e.g., ETH, USDT) need to exchange them for USDC before depositing .
* **Fee Estimation:** Users need to understand the complete fee structure and the final amount to be received before executing a deposit.
* **Quote Caching:** To avoid repeated queries, the system caches quotes for 60 seconds to improve response speed.

**DEX Aggregation Mechanism:**

* **Multi-Source Quotes:** Queries multiple DEX aggregators (1inch, Odos, etc.) simultaneously.
* **Optimal Selection:** Selects the best route based on expected output amount, price impact, Gas fees, etc.
* **Redis Caching:** Quotes with identical parameters are cached for 60 seconds.
* **Slippage Protection:** Default slippage is 0.5% (50bps), customizable .

**2. Request**

| Item            | Value                           | Note                      |
| --------------- | ------------------------------- | ------------------------- |
| **HTTP Method** | `POST`                          |                           |
| **URI**         | `/api/v1/partner/quote/deposit` |                           |
| **Auth Level**  | **Basic Auth**                  | No status restriction     |
| **Headers**     | `Content-Type`                  | Fixed: `application/json` |
|                 | `tokenId`                       | User's URID               |

**Request Parameters (DepositQuoteReq)**

| Parameter     | Type   | Required | Description                                                                                                              | Example                                              |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
| `chainId`     | string | **Yes**  | Source Chain ID (CAIP-2 format).                                                                                         | `"eip155:1"` (Ethereum), `"eip155:42161"` (Arbitrum) |
| `userAddress` | string | **Yes**  | User wallet address (used to build the route).                                                                           | `"0x1234...5678"`                                    |
| `fromToken`   | string | **Yes**  | Source token address (Use `0x00...00` for native tokens).                                                                | `"0xA0b8...B48"` (USDC)                              |
| `toToken`     | string | **Yes**  | Target fiat token contract address. See [token contract addresses](/api-reference/smart-contracts#contract-addresses-1). | `"0xD598...aCC"` (USD)                               |
| `amount`      | string | **Yes**  | Deposit amount (in smallest unit, e.g., Wei).                                                                            | `"1000000000000000000"` (1 ETH)                      |

**Parameter Notes:**

* **chainId:** Follows CAIP-2 standard (`eip155:<chainId>`). Common values: `eip155:1` (Ethereum), `eip155:5000` (Mantle), `eip155:42161` (Arbitrum One), `eip155:10` (Optimism).
* **fromToken:** Use the zero address `0x0000000000000000000000000000000000000000` for native tokens (e.g., ETH, MATIC).
* **amount:** The input token amount, must be in the token's smallest unit. (e.g., 1 ETH = 10^18 Wei; 1 USDC = 10^6 units).
* **slippageBps:** `slippageBps` is the maximum allowable deviation of the final output value from the estimated output during trade execution. This value determines the minOutputAmount. For example, 50 equals a 0.5% tolerance.
* **toToken:** You can get all the fiat token addresses from here: [token contract addresses](/api-reference/smart-contracts#contract-addresses-1)

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "",
  "result": {
    "quoteId": "ur_1772002152589294701",
    "chainId": "eip155:84532",
    "best": {
      "aggregator": "ur",
      "to": "0x0000000000000000000000000000000000000000",
      "swapCalldata": "0x",
      "minUsdcAmount": "4950000",
      "expectedUsdcAmount": "5000000",
      "slippageBps": 50,
      "deadline": 1772002211,
      "priceImpact": "0"
    },
    "inputAmount": "5000000",
    "outputAmount": "5",
    "exchangeRate": "1",
    "crossChainFee": "111598233453575",
    "networkFee": "3109867200000",
    "amountReceived": "4950000"
  },
  "timeNow": 1772002152589
}
```

**Response Parameters (DepositQuoteResp)**

| Parameter        | Type      | Description                                                                                                                 | Example                            |
| ---------------- | --------- | --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| `quoteId`        | string    | Unique quote identifier (Format: `<source>_<timestamp>`).                                                                   | `"1inch_1705..."`, `"ur_1705..."`  |
| `chainId`        | string    | Source Chain ID.                                                                                                            | `"eip155:1"`                       |
| `best`           | BestQuote | Details of the best quote (see table below).                                                                                |                                    |
| `networkFee`     | string    | Estimated network fee (denominated in Native Token); paid by the user from the source chain wallet.                         | `"1250000000000000"` (0.00125 ETH) |
| `crossChainFee`  | string    | Cross-chain fee (denominated in Native Token); paid by the user from the source chain wallet.                               | `"500000000000000"` (0.0005 ETH)   |
| `outputAmount`   | string    | The estimated net amount of target fiat currency to be received, after deducting all applicable fees from the 'best quote'. | `95.5` for 95.5 USD                |
| `inputAmount`    | string    | Deposit amount (in smallest unit, e.g., Wei).                                                                               | `"5000000"`                        |
| `exchangeRate`   | string    | Exchange rate (1 USDC = ? Target Fiat).                                                                                     | `"1.0"`                            |
| `amountReceived` | string    | The actual USDC amount received on Arbitrum (in smallest unit, e.g., Wei). Only returned for Tempo chain offramp.           | `"4950000"`                        |

**BestQuote Structure**

| Parameter            | Type   | Description                                                                                                                                                                                                                              | Example                              |
| -------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `aggregator`         | string | DEX aggregator name.                                                                                                                                                                                                                     | `"1inch"`, `"odos"`, `"ur"` (Direct) |
| `to`                 | string | Exchange contract address (Zero address for direct USDC deposit). If a user interacts with [offramp contract](/api-reference/smart-contracts#methods-4) directly, this value is required as the contract input parameter `\_aggregator`. | `"0x1111..."`                        |
| `swapCalldata`       | string | Call data for the exchange (`"0x"` for direct USDC deposit). If a user interacts with [offramp contract](/api-reference/smart-contracts#methods-4) directly, this value is required as the contract input parameter `\_swapCalldata`.    | `"0x12aa..."`                        |
| `minUsdcAmount`      | string | Minimum USDC output amount (after slippage).                                                                                                                                                                                             | `"990000000"` (990 USDC)             |
| `expectedUsdcAmount` | string | Expected USDC output amount.                                                                                                                                                                                                             | `"1000000000"` (1000 USDC)           |
| `slippageBps`        | int32  | Slippage basis points.                                                                                                                                                                                                                   | `50` (0.5%)                          |
| `deadline`           | int64  | Quote expiration timestamp (Unix seconds).                                                                                                                                                                                               | `1705123516`                         |
| `priceImpact`        | string | Price impact percentage.                                                                                                                                                                                                                 | `"0.05"` (0.05%)                     |

**4. Request Examples**

**Basic Example (ETH Deposit):**

```bash
curl -X POST 'https://urapi3-qa.ur-inc.xyz/api/v1/partner/quote/deposit' \
-H 'Content-Type: application/json' \
-H 'tokenId: 12345' \
-d '{
  "chainId": "eip155:1",
  "userAddress": "0x1234567890123456789012345678901234567890",
  "fromToken": "0x0000000000000000000000000000000000000000",
  "toToken": "USD",
  "amount": "1000000000000000000"
}'

```

**Arbitrum USDC Direct Deposit:**

```bash
curl -X POST 'https://urapi3-qa.ur-inc.xyz/api/v1/partner/quote/deposit' \
-H 'Content-Type: application/json' \
-H 'tokenId: 12345' \
-d '{
  "chainId": "eip155:42161",
  "userAddress": "0xYourWalletAddress",
  "fromToken": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
  "toToken": "USD",
  "amount": "1000000000"
}'

```

**5. Error Codes**

| Code    | Description            | Reason                                                    |
| ------- | ---------------------- | --------------------------------------------------------- |
| `10001` | Auth Failure           | Token invalid or expired.                                 |
| `10002` | Parse Error            | Malformed request body.                                   |
| `10006` | Status Not Supported   | Account not active (Non-Live status).                     |
| `10009` | Invalid Param          | Missing `chainId`/`fromToken`/`amount`, or `amount` <= 0. |
| `10051` | Invalid Chain ID       | Unsupported `chainId`.                                    |
| `10052` | No Available Quotes    | All DEX queries failed.                                   |
| `10053` | No Best Route          | Could not determine best route from DEX quotes.           |
| `10054` | Calculation Failed     | Failed to calculate network or cross-chain fees.          |
| `11006` | Insufficient Liquidity | Cross-chain liquidity is insufficient.                    |

**6. Common Error Troubleshooting**

| Error Message                      | Reason                                   | Solution                                                                  |
| ---------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------- |
| `"invalid params"`                 | `chainId`/`fromToken`/`amount` is empty. | Check if all required parameters are complete.                            |
| `"amounts must be greater than 0"` | `amount` <= 0 or format error.           | Ensure `amount` is a valid positive number string.                        |
| `"invalid chain"`                  | `chainId` not in supported list.         | Use a valid CAIP-2 format Chain ID.                                       |
| `"no available quotes"`            | All DEX aggregator queries failed.       | Check network connection or retry later; liquidity might be insufficient. |
| `"no best route found"`            | Cannot select best route from quotes.    | Retry with adjusted `amount`.                                             |
| `"calc fee amount: ..."`           | Fee calculation failed.                  | Check if Gas Price interface is available.                                |

#### 4.1.2 Initiate offramp

In **External Wallet Access Mode**, users are expected to interact directly with the smart contract via the Partner Frontend by default.

**Contract**: `depositTokenViaAggregator` on the Off-ramp contract. Contract addresses per chain: see [Deposit (off-ramp)](/api-reference/smart-contracts#deposit-off-ramp).

Contract Parameters:

| Parameter           | Type    | Required | Description                                               | Example                                              |
| ------------------- | ------- | -------- | --------------------------------------------------------- | ---------------------------------------------------- |
| `_inputToken`       | address | **Yes**  | Source token address (Use `0x00...00` for native tokens). | `"eip155:1"` (Ethereum), `"eip155:42161"` (Arbitrum) |
| `_outputToken`      | address | **Yes**  | Target fiat token identifier (Fiat type after deposit).   | `"0x1234...5678"`                                    |
| `_amount`           | uint256 | **Yes**  | Deposit amount (in smallest unit, e.g., Wei).             | For USDC: "10000000" as 10.000000                    |
| `_aggregator`       | address | **Yes**  | Exchange contract address get from the quote API          |                                                      |
| `_swapCalldata`     | bytes   | **Yes**  | Get from the quote API response                           | "0x" for USDC direct deposit.                        |
| `_minUsdcAmount`    | uint256 | Yes      | Get from the quote API response.                          |                                                      |
| `_feeAmountViaUsdc` | unit256 | Yes      | Put "0" when user call the contract directly.             |                                                      |

**For Tempo chain**, use `depositWithFee` on the Tempo Off-ramp contract. Contract addresses per chain: see [Deposit (off-ramp)](/api-reference/smart-contracts#deposit-off-ramp).

Contract Parameters:

| Parameter           | Type    | Required | Description                                                                                                                                                           |
| ------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `inputToken`        | address | **Yes**  | Source stablecoin address.                                                                                                                                            |
| `inputAmount`       | uint256 | **Yes**  | Deposit amount (in smallest unit, e.g., Wei).                                                                                                                         |
| `outputToken`       | address | **Yes**  | Target fiat token address.                                                                                                                                            |
| `minAmountReceived` | uint256 | **Yes**  | Minimum amount to receive on Arbitrum. Calculate from the quote API: `amountReceived` adjusted by `slippageBps`.                                                      |
| `refundAddress`     | address | **Yes**  | Address to receive refund of excess cross-chain fee. Usually the user's wallet address.                                                                               |
| `maxFeeUsdcAmount`  | uint256 | **Yes**  | First-hop (Tempo → Arbitrum) cross-chain fee budget (denominated in USDC, in smallest unit). Calculate from the quote API: `crossChainFee` adjusted by `slippageBps`. |
| `feeAmountViaUsdc`  | uint256 | **Yes**  | Put "0" when user calls the contract directly.                                                                                                                        |

If you still require the ability for users to complete Offramp operations via an API, please contact the UR team.

### 4.2 Offramp API for Solana

**Note:** **§4.2** applies to **Solana** only. Today, Solana deposit (offramp) supports **USDC on Solana** swapped into **fiat** balance on the UR side. Support for additional tokens via **Jupiter** (and similar aggregators) is planned; this page will be updated when that ships.

#### 4.2.1 Get quote

**1. Description**

Returns execution parameters and a **fiat-credit quote** for a Solana **USDC** deposit. The endpoint **does not submit** any on-chain transaction: it returns addresses, fee estimates, optional **server-built** unsigned Solana transactions, and rule flags the partner must respect when building or signing.

**2. Request**

| Item            | Value                      | Note                                                                                                                                                  |
| --------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| **HTTP Method** | `POST`                     |                                                                                                                                                       |
| **URI**         | `/v1/solana/deposit/quote` | Relative to Partner OpenAPI base URL                                                                                                                  |
| **Auth**        | Partner request signing    | [Part A: Partner authentication (UR-OPEN-API & webhooks)](/api-reference/signature-and-verify#part-a-partner-authentication-ur-open-api-and-webhooks) |
| **Headers**     | `Content-Type`             | Fixed: `application/json`                                                                                                                             |

**Request body parameters**

| Parameter          | Type   | Required | Description                                                                                                                                                                           |
| ------------------ | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `urId`             | int64  | **Yes**  | User URID.                                                                                                                                                                            |
| `solanaAddress`    | string | **Yes**  | User’s Solana address (Base58); must match the key that will sign the deposit transaction.                                                                                            |
| `usdcAmount`       | string | **Yes**  | Planned USDC input amount in **smallest units** (USDC uses **6** decimals), e.g. `"10000000"` for 10 USDC.                                                                            |
| `outputCurrency`   | string | **Yes**  | Target fiat currency code (e.g. `"USD"`).                                                                                                                                             |
| `minUsdcAmount`    | string | No       | Client-side slippage floor in smallest units; must not exceed `usdcAmount`. If omitted or `"0"`, the on-chain slippage check is skipped. See **minUsdcAmount notes** below.           |
| `network`          | string | No       | Solana cluster: `"mainnet"` (or `"mainnet-beta"`) for production, `"devnet"` for testing. **Defaults to `"devnet"` if omitted; production callers must pass `"mainnet"` explicitly.** |
| `computeUnitPrice` | uint64 | No       | Solana **priority fee** in **micro-lamports per compute unit**. If omitted, the server default is **1000**. Raise under congestion if needed.                                         |

**Parameter notes**

* **usdcAmount:** Minimum deposit is **5 USDC** (`"5000000"`). Requests below this threshold are rejected with code `20003`.
* **minUsdcAmount:** The on-chain contract checks `net_amount >= minUsdcAmount`, where `net_amount = usdcAmount - fee_amount`. Because `fee_amount` is deducted before the check, passing a `minUsdcAmount` equal to `usdcAmount` will cause the transaction to revert with `SlippageExceeded` when fees are non-zero. **Recommended:** omit this field (or pass `"0"`) for USDC-only deposits; only set it when integrating a swap path with meaningful slippage.
* **network:** The server resolves program addresses, RPC endpoints, and LayerZero configuration based on this value. Using the wrong value (or omitting it in production) will return `50002` (`solana chain config not found`).

**Example request body (mainnet)**

```json
{
  "urId": 7537715481,
  "solanaAddress": "9E6fwJhowA1QHwtUXWNF9LnzwE5nXv5AxteaUFErTaeT",
  "usdcAmount": "10000000",
  "outputCurrency": "USD",
  "network": "mainnet"
}
```

**Example request body (devnet)**

```json
{
  "urId": 7537715481,
  "solanaAddress": "9B1GtWbjXHnTmWeb945SKpmWsx3S5fBCdEdXaEXriAwn",
  "usdcAmount": "10000000",
  "outputCurrency": "USD",
  "network": "devnet",
  "computeUnitPrice": 1000
}
```

**3. Response**

Successful responses use `code: 0` and return details under `data`. Field names are stable; some **fiat / fee** fields may be omitted depending on environment.

**Example response (mainnet)**

```json
{
  "code": 0,
  "message": "",
  "data": {
    "network": "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
    "programId": "DgiyAgiEJDt4hXw56mpKDd7mcYRYUaxrKg3ConyeupYX",
    "oapp": "Gnd23LHfEWX4BU8haprQpwJUG3aMjnRF2v8H96YMKDD",
    "endpointProgramId": "76y77prsiCMvXMjuoZ5VRrhG5qYBrUMYTE5WgHqgjEn6",
    "dstEid": 30181,
    "usdcMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "depositUsdcAccount": "7bmkazNHa595zYyVRJ99PjXZuifu1U8FF98vxeTxGbeQ",
    "feeReceiver": "BbggVSW9ZVmeNG3Fsw4jnNiE9N3XRahjg62etMcxWW5u",
    "evmOutputToken": "0x4e32ce01bd170aa80e30885041af1b56a04ad141",
    "nativeFeeLamports": "5000000",
    "nativeFeeLamportsWithBuffer": "6000000",
    "rules": {
      "onlyUsdc": true,
      "swapDataMustBeEmpty": true,
      "jupiterRemainingAccountsLenMustBeZero": true,
      "userMustHaveUsdcAta": true,
      "feePayerMustBeUser": true
    },
    "transaction": "AQAAAA...base64...",
    "lastValidBlockHeight": 412280000,
    "outputAmount": "9.9",
    "outputAmountBeforeFee": "10",
    "exchangeRate": "1",
    "processingFee": "0.1",
    "networkFee": "0",
    "totalFee": "0.1"
  }
}
```

**Example response (devnet)**

```json
{
  "code": 0,
  "message": "",
  "data": {
    "network": "EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
    "programId": "FgWV99azAfJLSnSQprTKVyq7zCyLJEK4QiJ3ri3GPrAo",
    "oapp": "4j6at8yw9co4dzZhEH8qnQhwzVXgotW2CMbw8FcHkcJR",
    "endpointProgramId": "76y77prsiCMvXMjuoZ5VRrhG5qYBrUMYTE5WgHqgjEn6",
    "dstEid": 40246,
    "usdcMint": "4ArjZDc2B1jLXtqpKCkPfXrkkfFoqsi19bxcDoTJmpiL",
    "depositUsdcAccount": "76TAaJR4P5KQtow2WpVsFh3KShXVR2Ft6trWDVevX3Uo",
    "feeReceiver": "38Ygm7MRkDxBP4wGCgeSrR3atv3SyhachRRwAzMyF24P",
    "evmOutputToken": "0xdf79470986629ae4893BfCE0c6C0F4d085E99741",
    "nativeFeeLamports": "834617",
    "nativeFeeLamportsWithBuffer": "1001541",
    "rules": {
      "onlyUsdc": true,
      "swapDataMustBeEmpty": true,
      "jupiterRemainingAccountsLenMustBeZero": true,
      "userMustHaveUsdcAta": true,
      "feePayerMustBeUser": true
    },
    "transaction": "AQAAAA...base64...",
    "lastValidBlockHeight": 385942100,
    "outputAmount": "10",
    "outputAmountBeforeFee": "10",
    "exchangeRate": "1",
    "processingFee": "0.05",
    "networkFee": "0",
    "totalFee": "0"
  }
}
```

**Response fields (`data`)**

| Field                         | Type   | Description                                                                                                                                                                                                      |
| ----------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `network`                     | string | Solana genesis hash (first 32 bytes, Base58). Mainnet: `5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`; devnet: `EtWTRABZaYq6iMfeYKouRu166VU2xqa1`. Use this value to confirm the response matches the cluster you requested. |
| `programId`                   | string | Solana OApp program id (Base58).                                                                                                                                                                                 |
| `oapp`                        | string | Store PDA for the OApp (Base58).                                                                                                                                                                                 |
| `endpointProgramId`           | string | LayerZero Endpoint program id (Base58).                                                                                                                                                                          |
| `dstEid`                      | uint32 | LayerZero destination endpoint id (Mantle side).                                                                                                                                                                 |
| `usdcMint`                    | string | Solana USDC mint (Base58).                                                                                                                                                                                       |
| `depositUsdcAccount`          | string | USDC token account that receives the deposit (Base58).                                                                                                                                                           |
| `feeReceiver`                 | string | Protocol fee receiver (Base58).                                                                                                                                                                                  |
| `evmOutputToken`              | string | Mantle-side output token contract address (`0x…`).                                                                                                                                                               |
| `nativeFeeLamports`           | string | Estimated LayerZero messaging fee in **lamports** (1 SOL = 10^9 lamports); paid by the user (the user is the Solana transaction fee payer).                                                                      |
| `nativeFeeLamportsWithBuffer` | string | Recommended lamport budget with **\~20% buffer**; prefer passing this as the transaction’s native fee / value where applicable.                                                                                  |
| `rules`                       | object | Enforced constraints on the Solana side (see **Rules object** below).                                                                                                                                            |
| `transaction`                 | string | Optional Base64 **`VersionedTransaction` (v0)**, **unsigned**. If empty or missing, build the transaction manually from the other fields.                                                                        |
| `lastValidBlockHeight`        | uint64 | Upper bound block height for the blockhash inside `transaction`. If the chain passes this height before confirmation, **request a new quote**.                                                                   |
| `outputAmount`                | string | Estimated net fiat amount (may be omitted).                                                                                                                                                                      |
| `outputAmountBeforeFee`       | string | Fiat amount before fees (may be omitted).                                                                                                                                                                        |
| `exchangeRate`                | string | USDC → target fiat rate (may be omitted).                                                                                                                                                                        |
| `processingFee`               | string | Processing fee component (may be omitted).                                                                                                                                                                       |
| `networkFee`                  | string | EVM-side network fee; often `"0"` in Solana quote context (may be omitted).                                                                                                                                      |
| `totalFee`                    | string | Total fees (may be omitted).                                                                                                                                                                                     |

**Rules object (`data.rules`)**

| Field                                   | Type | Typical meaning                                                     |
| --------------------------------------- | ---- | ------------------------------------------------------------------- |
| `onlyUsdc`                              | bool | Deposit path must use USDC only.                                    |
| `swapDataMustBeEmpty`                   | bool | No inline swap payload unless officially supported.                 |
| `jupiterRemainingAccountsLenMustBeZero` | bool | Jupiter remaining accounts must be empty (future aggregator hooks). |
| `userMustHaveUsdcAta`                   | bool | User must already have a USDC ATA; create it first if missing.      |
| `feePayerMustBeUser`                    | bool | Fee payer on the Solana transaction must be the user.               |

**Response signature (optional)**

The server may return `x-api-signature` and `x-api-publicKey` headers so the client can verify the response body against UR’s signer (same pattern as other Partner OpenAPI responses).

**4. Error codes**

| Code    | Description                                                                                                      |
| ------- | ---------------------------------------------------------------------------------------------------------------- |
| `10001` | Partner authentication failed (invalid signature, wrong key, or expired deadline).                               |
| `20003` | Invalid parameters: bad `urId`, malformed address, unsupported `outputCurrency`, or user not under this partner. |
| `50002` | Internal configuration error (e.g. missing Solana program config).                                               |
| `50003` | On-chain RPC failure or quote engine failure.                                                                    |

**5. Common troubleshooting**

| Symptom                                        | Likely cause                                                                                                             | What to do                                                                                                                                                                     |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `50002` (`solana chain config not found`)      | `network` field is missing or wrong; omitting it defaults to `"devnet"` which does not exist in production config.       | Pass `"network": "mainnet"` explicitly for production calls.                                                                                                                   |
| `20003` (`amount must be at least 5 USDC`)     | `usdcAmount` is below the minimum 5 USDC (`5000000`).                                                                    | Increase the amount to at least `"5000000"`.                                                                                                                                   |
| `20003` (`invalid minUsdcAmount`)              | `minUsdcAmount` is non-numeric or exceeds `usdcAmount`.                                                                  | Omit the field or pass a value ≤ `usdcAmount`.                                                                                                                                 |
| `SlippageExceeded` (on-chain `0x1773`)         | `minUsdcAmount` set to `usdcAmount` while on-chain fee is > 0; contract checks `net_amount (post-fee) >= minUsdcAmount`. | Omit `minUsdcAmount` or set it to `"0"` for USDC-only deposits.                                                                                                                |
| `20003` after fixing params                    | User record not provisioned for this partner / chain                                                                     | Ensure the user exists for this partner (provisioning flows are documented elsewhere).                                                                                         |
| Transaction never lands                        | Blockhash expired                                                                                                        | Compare current block height to `lastValidBlockHeight`; call quote again.                                                                                                      |
| Simulation failure (`ProgramFailedToComplete`) | Compute unit limit too low or missing USDC ATA.                                                                          | The server pre-built transaction already includes a `SetComputeUnitLimit` instruction; if building manually, set at least **400,000 CU**. Also ensure the user has a USDC ATA. |
| Simulation failure (other)                     | Wrong signer or account mismatch                                                                                         | Obey `rules`; ensure `solanaAddress` matches the signing key.                                                                                                                  |

#### 4.2.2 Execute offramp transaction

After a successful quote, the partner (or user wallet) must **sign and submit** the Solana transaction. UR recommends using the **pre-built** `data.transaction` when it is present.

**Recommended path: pre-built transaction**

When `data.transaction` is non-empty:

1. Base64-decode to bytes and deserialize with `@solana/web3.js` `VersionedTransaction.deserialize`.
2. **Before signing**, run structural validation against the expected deposit layout (see **§4.2.3**), including `validateDepositViaAggregatorTx(tx, quote.programId)` when using the reference sketch.
3. **Recommended before signing:** refresh **`recentBlockhash`** via `connection.getLatestBlockhash` and rebuild the `VersionedTransaction` (same compiled instructions). Quote responses may sit for seconds before the user signs; a fresh blockhash improves landing rate and pairs with a current `lastValidBlockHeight` for confirmation.
4. Sign with the **user’s** Solana key (must match `solanaAddress` in the quote request).
5. `simulateTransaction` with `sigVerify: true` before broadcast.
6. `sendTransaction` to the correct RPC for the quoted `network`.
7. Poll confirmation using `blockhash` / `lastValidBlockHeight` that match the signed transaction (from step 3 if you refreshed; otherwise from the quote). If the blockhash expires before confirmation, obtain a **new quote** and retry.

If `data.transaction` is empty, assemble the deposit instruction manually using `programId`, accounts, `rules`, and fees; contact UR for bytecode-level docs if you are not using the pre-built path.

**Example (Node.js)**

Dependencies: `@solana/web3.js` (with `TransactionMessage.decompile` / `compileToV0Message`, as in recent releases) and a Partner OpenAPI client that implements request signing as in **§4.2.1**.

```javascript
import {
  Connection,
  VersionedTransaction,
  TransactionMessage,
} from '@solana/web3.js'
import { validateDepositViaAggregatorTx } from './solana-deposit-tx-validator.mjs' // see §4.2.3 below

// Use the correct RPC for your target network:
// Mainnet: 'https://api.mainnet-beta.solana.com' (or your preferred RPC provider)
// Devnet:  'https://api.devnet.solana.com'
const SOLANA_RPC_URL = 'https://api.mainnet-beta.solana.com'

async function depositWithPrebuiltTx({
  callOpenApi,
  solanaKeypair,
  quoteRequestBody,
}) {
  const connection = new Connection(SOLANA_RPC_URL, 'confirmed')

  const quoteResp = await callOpenApi('/v1/solana/deposit/quote', quoteRequestBody)
  const quote = quoteResp.body.data

  if (!quote?.transaction)
    throw new Error('Server did not return a pre-built transaction; build manually or retry later.')

  const tx = VersionedTransaction.deserialize(
    Buffer.from(quote.transaction, 'base64')
  )

  validateDepositViaAggregatorTx(tx, quote.programId)

  const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('recent')

  tx.message.recentBlockhash = blockhash;

  tx.sign([solanaKeypair]);

  const sim = await connection.simulateTransaction(tx, { sigVerify: true })
  if (sim.value.err)
    throw new Error(`simulate failed: ${JSON.stringify(sim.value.err)}`)

  const signature = await connection.sendTransaction(tx, {
    maxRetries: 5,
    skipPreflight: false,
  })

  await connection.confirmTransaction(
    { signature, blockhash, lastValidBlockHeight },
    'confirmed'
  )

  return signature
}
```

Pass `quoteRequestBody` with the same shape as **§4.2.1** (including `urId`, `solanaAddress`, `usdcAmount`, `outputCurrency`, and optional `network` / `computeUnitPrice`). Use a **partner-signed** `callOpenApi` implementation consistent with your environment. Implement `validateDepositViaAggregatorTx` as in **§4.2.3** (or import an equivalent module).

#### 4.2.3 Security and validation (strongly recommended)

Malicious browser extensions, compromised front-end bundles, or man-in-the-middle proxies could **swap** the Base64 transaction returned by `/v1/solana/deposit/quote` for a different `VersionedTransaction` before the user signs. To reduce that risk, **always validate** the instruction layout and program IDs **after** Base64 decode and **before** any signature.

**What a legitimate UR pre-built deposit transaction looks like**

For the current USDC-only path, UR expects a **`VersionedTransaction` (v0)** whose **compiled instructions** are **exactly three**:

| Index | Expected program                                                       | Role                                                            |
| ----- | ---------------------------------------------------------------------- | --------------------------------------------------------------- |
| `0`   | `ComputeBudget` (`ComputeBudgetProgram.programId`)                     | Typically `SetComputeUnitLimit`                                 |
| `1`   | `ComputeBudget`                                                        | Typically `SetComputeUnitPrice` (priority fee)                  |
| `2`   | UR deposit program (`data.programId` from the **same** quote response) | `deposit_via_aggregator` (Anchor discriminator + Borsh payload) |

Reject transactions with fewer than three instructions, more than three instructions, or any program id on the third instruction that does **not** match the `programId` returned alongside `transaction` in that quote.

**Address lookup tables (ALT)**

The reference `programIdForIx` helper below resolves each instruction’s program id from **`message.staticAccountKeys` only**. That matches **typical UR pre-built** `data.transaction` payloads from this endpoint, where each of the three instructions’ `programIdIndex` values point into `staticAccountKeys` (no extra lookup-table resolution needed).

If you deserialize a transaction where `programIdIndex >= staticAccountKeys.length`, the program id is supplied through one or more **address lookup tables**. In that case you must resolve the full account key list (for example via `VersionedTransaction` + loaded lookup tables, or APIs that hydrate `MessageV0` account keys) and compute the program id for each instruction from that resolved list, then compare instruction 2 to `quote.programId`. Until you extend the sketch that way, the validator may **throw** or **reject** a transaction that is valid on-chain.

**Program id by environment (reference)**

Always treat the **`programId` field in the quote response** as the source of truth. Published examples:

| Network             | `programId` (Base58)                           | USDC Mint                                      |
| ------------------- | ---------------------------------------------- | ---------------------------------------------- |
| Solana mainnet-beta | `DgiyAgiEJDt4hXw56mpKDd7mcYRYUaxrKg3ConyeupYX` | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |
| Solana devnet       | `FgWV99azAfJLSnSQprTKVyq7zCyLJEK4QiJ3ri3GPrAo` | `4ArjZDc2B1jLXtqpKCkPfXrkkfFoqsi19bxcDoTJmpiL` |

**Instruction data (third instruction)**

The third instruction’s `data` should decode as **`deposit_via_aggregator`**: 8-byte Anchor discriminator (first 8 bytes of `sha256("global:deposit_via_aggregator")`), followed by Borsh fields such as `inputAmount`, `minUsdcAmount`, `evmRecipient`, `evmOutputToken`, `nativeFee`, `swapData` length and payload, and `jupiterRemainingAccountsLen`. For the current USDC-only ruleset, `data.rules` from the quote may require empty swap/Jupiter tails; align validation with those flags when you decode.

**Reference validation sketch (Node.js)**

The following pattern matches production checks when program ids are indexed via **`staticAccountKeys`**: assert `VersionedTransaction`, resolve each instruction’s program id from `message.staticAccountKeys`, require instructions 0–1 to be Compute Budget and instruction 2 to equal `expectedProgramIdFromQuote`, then decode the third instruction’s data and ensure the discriminator matches `deposit_via_aggregator`. See **Address lookup tables (ALT)** above if you need full lookup resolution.

```javascript
import {
  VersionedTransaction,
  ComputeBudgetProgram,
  PublicKey,
} from '@solana/web3.js'

const DEPOSIT_VIA_AGGREGATOR_DISC = Buffer.from([
  0xc7, 0xb7, 0x9c, 0x2b, 0x4a, 0x6d, 0x26, 0x01,
]) // first 8 bytes of sha256("global:deposit_via_aggregator")

function programIdForIx(message, programIdIndex) {
  const keys = message.staticAccountKeys
  if (programIdIndex >= keys.length)
    throw new Error('programIdIndex requires ALT resolution; extend validator if you use lookups')
  return keys[programIdIndex].toBase58()
}

/**
 * @param {VersionedTransaction} tx
 * @param {string} expectedProgramIdFromQuote - data.programId from the same /v1/solana/deposit/quote response as data.transaction
 */
export function validateDepositViaAggregatorTx(tx, expectedProgramIdFromQuote) {
  if (!(tx instanceof VersionedTransaction))
    throw new Error('expected VersionedTransaction')

  const { message } = tx
  const ixs = message.compiledInstructions
  if (ixs.length !== 3)
    throw new Error(`expected exactly 3 compiled instructions, got ${ixs.length}`)

  const id0 = programIdForIx(message, ixs[0].programIdIndex)
  const id1 = programIdForIx(message, ixs[1].programIdIndex)
  const id2 = programIdForIx(message, ixs[2].programIdIndex)

  if (!ComputeBudgetProgram.programId.equals(new PublicKey(id0)))
    throw new Error(`instruction 0 must be ComputeBudget, got ${id0}`)
  if (!ComputeBudgetProgram.programId.equals(new PublicKey(id1)))
    throw new Error(`instruction 1 must be ComputeBudget, got ${id1}`)
  if (!new PublicKey(expectedProgramIdFromQuote).equals(new PublicKey(id2)))
    throw new Error(`instruction 2 programId mismatch: expected ${expectedProgramIdFromQuote}, got ${id2}`)

  const data = ixs[2].data
  if (data.length < 8 || !Buffer.from(data.slice(0, 8)).equals(DEPOSIT_VIA_AGGREGATOR_DISC))
    throw new Error('instruction 2 is not deposit_via_aggregator')

  return true
}
```

Call `validateDepositViaAggregatorTx(tx, quote.programId)` immediately after `VersionedTransaction.deserialize(...)`, then proceed to `tx.sign([...])`. You can extend the decoder to compare `inputAmount` / `minUsdcAmount` / `nativeFee` against the user’s intended quote parameters for defense in depth.

## 5. Onramp

{% hint style="warning" %}
**Available soon.** On-ramp (fiat-to-crypto) is not yet available for integration and will be enabled in a future release. The reference below is provided for preview only.
{% endhint %}

This section documents Fiat-to-Crypto Onramp in **External Wallet Access Mode**.

### 5.1 Onramp API

#### 5.1.1 Get onramp limit

**1. Description** Returns user-specific onramp eligibility signals and amount caps.\
This endpoint does **not** return a single `isSupport` boolean. Integrators must evaluate:

* `regionLocked`
* `usdcDepegged`
* `livenessLocked`
* `maxAmounts`
* `minAmounts`

**2. Request**

| Item            | Value                  | Note                      |
| --------------- | ---------------------- | ------------------------- |
| **HTTP Method** | `GET`                  |                           |
| **URI**         | `/api/v1/onramp-limit` |                           |
| **Auth Level**  | **Full Auth**          | Live account required     |
| **Headers**     | `Content-Type`         | Fixed: `application/json` |
|                 | `tokenId`              | User‘s URID               |
|                 | `network`              | Network Identifier        |
|                 | `sign/hash/deadline`   | Full Auth signature       |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "livenessLocked": false,
    "livenessLockMins": 0,
    "maxAmounts": {
      "USD": "50000",
      "EUR": "46500"
    },
    "minAmounts": {
      "USD": "5",
      "EUR": "4.65"
    },
    "usdcDepegged": false,
    "regionLocked": false
  },
  "timeNow": 1703123456789
}
```

**4. Result Fields**

| Field Name         | Type               | Description                                                     |
| ------------------ | ------------------ | --------------------------------------------------------------- |
| `livenessLocked`   | bool               | Whether liveness is currently locked for this user.             |
| `livenessLockMins` | int64              | Remaining lock duration in minutes (`0` if unlocked).           |
| `maxAmounts`       | map\[string]string | Max allowed onramp amount by fiat currency (no token decimals). |
| `minAmounts`       | map\[string]string | Min allowed onramp amount by fiat currency (no token decimals). |
| `usdcDepegged`     | bool               | Whether USDC depeg protection is active.                        |
| `regionLocked`     | bool               | Whether onramp is region-restricted for this user.              |

Read `maxAmounts` and `minAmounts` per fiat currency; do not hardcode them. UR derives both from a single USD anchor (about 5 USD minimum; the maximum depends on the user's liveness state) and converts each to fiat at the live exchange rate, so the values differ across currencies and move with rates.

**5. Request Example**

```bash
curl -X GET 'https://api.ur.app/api/v1/onramp-limit' \
-H 'Content-Type: application/json' \
-H 'tokenId: 12345' \
-H 'network: 5000' \
-H 'hash: 0x...' \
-H 'sign: 0x...' \
-H 'deadline: 1703123456789'
```

**6. Error Codes**

| Code    | Description   | Reason                                    |
| ------- | ------------- | ----------------------------------------- |
| `10001` | Auth Failure  | Invalid authentication or signature.      |
| `10002` | Parse Error   | Invalid request format.                   |
| `10000` | Service Error | Internal failure when calculating limits. |

#### 5.1.2 Get onramp quote

**1. Description** Returns quote for both:

* `scene=onramp`: Fiat token -> destination token
* `scene=swap_retry`: retry swap using destination-chain USDC

The response includes `needLiveness`.\
Quote results are cached server-side and later validated by submit/retry endpoints.

**2. Request**

| Item            | Value                  | Note                      |
| --------------- | ---------------------- | ------------------------- |
| **HTTP Method** | `POST`                 |                           |
| **URI**         | `/api/v1/quote/onramp` |                           |
| **Auth Level**  | **Full Auth**          | Live account required     |
| **Headers**     | `Content-Type`         | Fixed: `application/json` |
|                 | `tokenId`              | User's URID               |
|                 | `network`              | Network Identifier        |
|                 | `sign/hash/deadline`   | Full Auth signature       |

**Request Parameters**

* You can get all the fiat token addresses from here: [token contract addresses](/api-reference/smart-contracts#contract-addresses-1)
* The `amount` in the request body is the smallest unit, that means `10000` USD here equals 100 USD

```json
{
  "scene": "onramp",
  "srcChainId": "eip155:5000",
  "dstChainId": "eip155:8453",
  "fromToken": "FiatTokenAddress",
  "toToken": "0xDestinationTokenAddress",
  "amount": "100000000",
  "slippageBps": 50
}
```

**3. Response**

* The `*AmountOut` values in the response body is in the smallest unit. For example: `12345000` USDC equals 12.345 USDC.

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "quoteId": "onramp_direct_1703123000000_12345",
    "srcChainId": "eip155:5000",
    "dstChainId": "eip155:8453",
    "best": {
      "aggregator": "1inch",
      "to": "0xAggregatorAddress",
      "swapCalldata": "0x....",
      "expectedAmountOut": "1234500",
      "minAmountOut": "1228327",
      "slippageBps": 50,
      "deadline": 1703123600,
      "priceImpact": "0.12"
    },
    "allQuotes": [
      {
        "source": "1inch",
        "expectedAmountOut": "1234500",
        "priceImpact": "0.12"
      }
    ],
    "outputAmount": "1.2345",
    "exchangeRate": "0.0123",
    "displayRate": "0.0123",
    "displayUnit": 1,
    "needLiveness": true,
    "networkFee": "0.43",
    "processingFee": "0.50",
    "totalFee": "0.43",
    "warningMessage": ""
  },
  "timeNow": 1703123456789
}
```

* Key Fields

| Field Name          | Type   | Description                                                                                                                                                       |
| ------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quoteId`           | string | Quote identifier used by submit/retry endpoints.                                                                                                                  |
| `best.to`           | string | The value of `dstAggregator` in the Onramp request API                                                                                                            |
| `best.swapCalldata` | string | The value of `dstSwapCalldata` in the Onramp request API                                                                                                          |
| `outputAmount`      | string | The estmated human readable output amount                                                                                                                         |
| `exchangeRate`      | string | Includes the swap price impact and spreads                                                                                                                        |
| `displayRate`       | string | When the `displayUnit`==1, same as `exchangeRate`, but if the input currency is JPY, the `displayUnit` will be 100, and displayRate will be 100JPY -> 1 XXX token |
| `processingFee`     | string | Charged for normal users, pro users can ignore                                                                                                                    |
| `networkFee`        | string | Includes the estimated gas fee and crosschain fee, deducted from the user's input fiat.                                                                           |
| `needLiveness`      | bool   | If `true`, user must pass liveness before onramp submit.                                                                                                          |

**4. Request Example**

```bash
curl -X POST 'https://api.ur.app/api/v1/quote/onramp' \
-H 'Content-Type: application/json' \
-H 'tokenId: 12345' \
-H 'network: 5000' \
-H 'hash: 0x...' \
-H 'sign: 0x...' \
-H 'deadline: 1703123456789' \
-d '{
  "scene": "onramp",
  "srcChainId": "eip155:5000",
  "dstChainId": "eip155:8453",
  "fromToken": "0xFiatTokenAddress",
  "toToken": "0xDestinationTokenAddress",
  "amount": "100000000",
  "slippageBps": 50
}'
```

**5. Error Codes**

| Code    | Description     | Reason                                                          |
| ------- | --------------- | --------------------------------------------------------------- |
| `10001` | Auth Failure    | Invalid authentication or signature.                            |
| `10002` | Parse Error     | Malformed request body.                                         |
| `10009` | Invalid Param   | Unsupported scene/token/chain, empty fields, or invalid amount. |
| `10040` | Quote Try Again | Quote unavailable/expired; request a new quote.                 |
| `10000` | Service Error   | Internal quote computation failure.                             |

#### 5.1.3 Get liveness token (onramp compliance)

**1. Description** Creates a Sumsub liveness access token for onramp flow. Call this only when quote returns `needLiveness=true`.

**2. Request**

| Item            | Value                        | Note                      |
| --------------- | ---------------------------- | ------------------------- |
| **HTTP Method** | `GET`                        |                           |
| **URI**         | `/api/v2/get-liveness-token` |                           |
| **Auth Level**  | **Full Auth**                | Live account required     |
| **Headers**     | `Content-Type`               | Fixed: `application/json` |
|                 | `tokenId`                    | User's URID               |
|                 | `network`                    | Network Identifier        |
|                 | `sign/hash/deadline`         | Full Auth signature       |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "vendor": "sumsub",
    "access_token": "sumsub_access_token_xxx",
    "user_id": "sumsub_user_id_xxx"
  },
  "timeNow": 1703123456789
}
```

**4. Request Example**

```bash
curl -X GET 'https://api.ur.app/api/v2/get-liveness-token' \
-H 'Content-Type: application/json' \
-H 'tokenId: 12345' \
-H 'network: 5000' \
-H 'hash: 0x...' \
-H 'sign: 0x...' \
-H 'deadline: 1703123456789'
```

**5. Error Codes**

| Code    | Description   | Reason                                               |
| ------- | ------------- | ---------------------------------------------------- |
| `10001` | Auth Failure  | Invalid authentication or signature.                 |
| `10000` | Service Error | Liveness locked or upstream token generation failed. |

#### 5.1.4 Check liveness result

**1. Description** Polls latest liveness status for current user in onramp scenario.

**2. Request**

| Item            | Value                           | Note                      |
| --------------- | ------------------------------- | ------------------------- |
| **HTTP Method** | `GET`                           |                           |
| **URI**         | `/api/v2/check-liveness-result` |                           |
| **Auth Level**  | **Full Auth**                   | Live account required     |
| **Headers**     | `Content-Type`                  | Fixed: `application/json` |
|                 | `tokenId`                       | User's URID               |
|                 | `network`                       | Network Identifier        |
|                 | `sign/hash/deadline`            | Full Auth signature       |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "liveness_result": "pass",
    "checked_at": 1703123000,
    "expired_at": 1703727800,
    "liveness_fail_reason": "",
    "liveness_locked": false,
    "liveness_unlock_at": 0
  },
  "timeNow": 1703123456789
}
```

**4. Result Fields**

| Field Name             | Type   | Description                                                    |
| ---------------------- | ------ | -------------------------------------------------------------- |
| `liveness_result`      | string | Liveness status (`pass`, `pending`, `rejected`, etc.).         |
| `checked_at`           | int64  | Unix seconds when latest liveness check was processed.         |
| `expired_at`           | int64  | Unix seconds when current pass status expires (if applicable). |
| `liveness_fail_reason` | string | Failure reason if rejected.                                    |
| `liveness_locked`      | bool   | Lock status due to repeated failures.                          |
| `liveness_unlock_at`   | int64  | Unix seconds when lock ends (if locked).                       |

**5. Error Codes**

| Code    | Description              | Reason                               |
| ------- | ------------------------ | ------------------------------------ |
| `10001` | Auth Failure             | Invalid authentication or signature. |
| `10041` | Liveness Check Not Found | No liveness record yet.              |
| `10000` | Service Error            | Internal liveness query failure.     |

#### 5.1.5 Initiate onramp with permit

**1. Description** Submits onramp transaction with permit signature.\
Requires valid cached quote and (if required) passed liveness.

**2. Request**

| Item            | Value                        | Note                      |
| --------------- | ---------------------------- | ------------------------- |
| **HTTP Method** | `POST`                       |                           |
| **URI**         | `/api/v1/onramp-with-permit` |                           |
| **Auth Level**  | **Full Auth**                | Live account required     |
| **Headers**     | `Content-Type`               | Fixed: `application/json` |
|                 | `tokenId`                    | User's URID               |
|                 | `network`                    | Network Identifier        |
|                 | `sign/hash/deadline`         | Full Auth signature       |

**Request Parameters**

```json
{
  "quoteId": "onramp_direct_1703123000000_12345",
  "chainId": "eip155:5000",
  "tokenIn": "0xFiatTokenAddress",
  "amountIn": "100000000",
  "dstChainId": "eip155:8453",
  "dstAggregator": "0xAggregatorAddress",
  "dstTokenOut": "0xDestinationTokenAddress",
  "dstSwapCalldata": "0x....",
  "dstMinAmountOut": "1228327",
  "permitDeadline": 1703123900,
  "permitV": 28,
  "permitR": "0x....",
  "permitS": "0x...."
}
```

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "txHash": "0xabc123..."
  },
  "timeNow": 1703123456789
}
```

**4. Important Notes**

* Backend validates request fields against quote cache (`quoteId` binding).
* If quote expired or mismatched, submit is rejected.
* If eligibility says liveness is still required, submit is rejected.

**5. Error Codes**

| Code    | Description     | Reason                                                         |
| ------- | --------------- | -------------------------------------------------------------- |
| `10001` | Auth Failure    | Invalid authentication or signature.                           |
| `10002` | Parse Error     | Malformed request body.                                        |
| `10009` | Invalid Param   | Invalid amount/address/permit fields, or quote mismatch.       |
| `10040` | Quote Try Again | Quote expired; request a new quote.                            |
| `10000` | Service Error   | Eligibility check/permit validation/onchain submission failed. |

#### 5.1.6 Check pending retry

**1. Description** Returns one pending onramp retry candidate (if any).\
This endpoint is used for post-onramp retry handling when destination swap previously failed.

**2. Request**

| Item            | Value                          | Note                      |
| --------------- | ------------------------------ | ------------------------- |
| **HTTP Method** | `GET`                          |                           |
| **URI**         | `/api/v1/onramp/pending-retry` |                           |
| **Auth Level**  | **Full Auth**                  | Live account required     |
| **Headers**     | `Content-Type`                 | Fixed: `application/json` |
|                 | `tokenId`                      | User's URID               |
|                 | `network`                      | Network Identifier        |
|                 | `sign/hash/deadline`           | Full Auth signature       |

**3. Response**

If no retry is available:

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": null,
  "timeNow": 1703123456789
}
```

If retry is available:

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "originalTxHash": "0xoriginal...",
    "originalChainId": "eip155:5000",
    "originalToken": "0xFiatTokenAddress",
    "chainId": "eip155:8453",
    "fromToken": "0xUSDCAddressOnDstChain",
    "toToken": "0xDestinationTokenAddress",
    "amount": "99800000",
    "failedAt": 1703123000
  },
  "timeNow": 1703123456789
}
```

**4. Error Codes**

| Code    | Description   | Reason                               |
| ------- | ------------- | ------------------------------------ |
| `10001` | Auth Failure  | Invalid authentication or signature. |
| `10000` | Service Error | Internal retry query failure.        |

#### 5.1.7 Retry swap with permit

**1. Description** Executes destination-chain swap retry for a failed onramp record.

**2. Request**

| Item            | Value                             | Note                      |
| --------------- | --------------------------------- | ------------------------- |
| **HTTP Method** | `POST`                            |                           |
| **URI**         | `/api/v1/onramp-swap-with-permit` |                           |
| **Auth Level**  | **Full Auth**                     | Live account required     |
| **Headers**     | `Content-Type`                    | Fixed: `application/json` |
|                 | `tokenId`                         | User's URID               |
|                 | `network`                         | Network Identifier        |
|                 | `sign/hash/deadline`              | Full Auth signature       |

**Request Parameters**

```json
{
  "quoteId": "1inch_1703123000000_12345",
  "chainId": "eip155:8453",
  "originalTxHash": "0xoriginal...",
  "usdcAmount": "99800000",
  "tokenOut": "0xDestinationTokenAddress",
  "minAmountOut": "1228327",
  "aggregator": "0xAggregatorAddress",
  "swapCalldata": "0x....",
  "permitDeadline": 1703123900,
  "permitV": 28,
  "permitR": "0x....",
  "permitS": "0x...."
}
```

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "txHash": "0xretrytx..."
  },
  "timeNow": 1703123456789
}
```

**4. Important Notes**

* Retry call requires a valid retry quote from `scene=swap_retry`.
* Request fields must match quote cache and original retry record.
* Do not call this endpoint directly without pending-retry + quote preparation.

**5. Error Codes**

| Code    | Description     | Reason                                                                   |
| ------- | --------------- | ------------------------------------------------------------------------ |
| `10001` | Auth Failure    | Invalid authentication or signature.                                     |
| `10002` | Parse Error     | Malformed request body.                                                  |
| `10009` | Invalid Param   | Invalid amount/address/permit fields, quote mismatch, or state mismatch. |
| `10040` | Quote Try Again | Retry quote expired; request a new retry quote.                          |
| `10000` | Service Error   | Internal retry validation or onchain submit failure.                     |

#### 5.1.8 Cancel retry

**1. Description** Cancels a pending retry (`can_retry`) for an onramp transaction.

**2. Request**

| Item            | Value                         | Note                      |
| --------------- | ----------------------------- | ------------------------- |
| **HTTP Method** | `POST`                        |                           |
| **URI**         | `/api/v1/onramp/retry/cancel` |                           |
| **Auth Level**  | **Full Auth**                 | Live account required     |
| **Headers**     | `Content-Type`                | Fixed: `application/json` |
|                 | `tokenId`                     | User's URID               |
|                 | `network`                     | Network Identifier        |
|                 | `sign/hash/deadline`          | Full Auth signature       |

**Request Parameters**

```json
{
  "originalTxHash": "0xoriginal..."
}
```

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": null,
  "timeNow": 1703123456789
}
```

**4. Important Notes**

* This is **not** the default path when no pending record exists.
* Use only when a retry-eligible transaction exists and user explicitly abandons retry.

**5. Error Codes**

| Code    | Description   | Reason                                             |
| ------- | ------------- | -------------------------------------------------- |
| `10001` | Auth Failure  | Invalid authentication or signature.               |
| `10002` | Parse Error   | Malformed request body.                            |
| `10009` | Invalid Param | Missing or invalid `originalTxHash`.               |
| `10000` | Service Error | Internal query/update failure during cancellation. |

### 5.2 End-to-end flow

#### 5.2.1 Recommended main flow

1. Call `GET /api/v1/onramp-limit`.
2. Call `POST /api/v1/quote/onramp` with `scene=onramp`.
3. If quote returns `needLiveness=true`:

* Call `GET /api/v2/get-liveness-token`.
* Run Sumsub SDK in client.

  Example for Sumsub SDK calling:

  ```typescript
    const accessToken = await getAccessToken();
    const snsWebSdkInstance = snsWebSdk
      .init(accessToken, getAccessToken)
      .withConf({
        lang: "en",
        theme: "light",
      })
      .withOptions({ addViewportTag: false, adaptIframeHeight: true })
      .on("idCheck.onApplicantActionCompleted", (payload: any) => {
        if (payload?.answer === "GREEN") {
          // After successful verification, you can perform post-success actions here
        }
      })
      .build();
    snsWebSdkInstance.launch("#sumsub-websdk-container");
  ```
* After the user completes the above Sumsub process, step 2 (`POST /api/v1/quote/onramp`) can be called again. When `needLiveness = false`, the user may proceed with the transaction.

4. When liveness result is `pass` (or not required), call `POST /api/v1/onramp-with-permit`.

#### 5.2.2 Recommended retry flow

1. Periodically call `GET /api/v1/onramp/pending-retry`.
2. If response contains pending object:

* Call `POST /api/v1/quote/onramp` with `scene=swap_retry`.
* Call `POST /api/v1/onramp-swap-with-permit`.

3. If user decides not to retry, call `POST /api/v1/onramp/retry/cancel`.

### 5.3 Error codes & troubleshooting

#### Common codes in onramp

| Code    | Description           | Typical Meaning                                      |
| ------- | --------------------- | ---------------------------------------------------- |
| `10000` | DefError              | Generic backend/internal failure.                    |
| `10001` | AuthenticationFailure | Invalid token/signature/header auth data.            |
| `10002` | ParseRequestFailed    | Invalid request shape or JSON parse failure.         |
| `10009` | ParamInvalid          | Invalid parameter values or business-state mismatch. |
| `10040` | QuoteTryAgain         | Quote expired/unavailable; request a fresh quote.    |
| `10041` | LivenessCheckNotFound | No liveness record available yet.                    |

#### Troubleshooting matrix

| Error Message / Signal                       | Reason                                                     | Solution                                                                          |
| -------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `needLiveness=true` in quote                 | User amount/eligibility requires liveness verification     | Complete liveness flow before submit.                                             |
| `Face verification required`                 | Submit attempted before passing required liveness          | Poll liveness result; submit only after `liveness_result=pass`.                   |
| `Quote expired. Please request a new quote.` | Quote cache missing/expired/mismatch                       | Re-request quote and submit with matching parameters.                             |
| Empty result from `pending-retry`            | No retry-eligible record currently (`can_retry` not found) | Continue periodic polling; do not call retry endpoint directly.                   |
| Retry submit rejected                        | Missing retry quote or quote/original record mismatch      | Re-run retry sequence: pending-retry -> quote(scene=swap\_retry) -> retry submit. |

## 6. Bank transfer

This section covers payout transfer flows for existing external-wallet users. For recipient/contact listing, use the existing [Get User Profile](#id-3.1.1-get-user-profile) endpoint and its `contacts` field.

### 6.1 Create/select recipient

#### 6.1.1 Get supported banks

**1. Description** Returns supported banks and country metadata (including whether IBAN metadata is available).

**2. Request**

| Item            | Value           | Note |
| --------------- | --------------- | ---- |
| **HTTP Method** | `GET`           |      |
| **URI**         | `/api/v1/banks` |      |
| **Auth Level**  | **No Auth**     |      |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "",
  "result": [
    {
      "key": "AT",
      "name": "Austria",
      "countryCode": {
        "iso2": "AT",
        "iso3": "AUT"
      },
      "ibanMetadata": {
        "placeholder": "AT00 0000 0000 0000 0000",
        "mask": "{AT}00 **** **** **** ****",
        "bankCode": {
          "length": 5,
          "startDigit": 4
        }
      }
    },
    {
      "key": "HK",
      "name": "Hong Kong",
      "countryCode": {
        "iso2": "HK",
        "iso3": "HKG"
      },
      "banks": [
        {
          "name": "Bank of China HK (中银香港)",
          "bankCode": "012",
          "bic": "BKCHHKHHXXX",
          "country": "HK",
          "accountMask": "\\012-000000000[000]",
          "accountPlaceholder": "012-000000000000",
          "accountNotice": ""
        },
        {
          "name": "Hang Seng Bank (恒生银行)",
          "bankCode": "024",
          "bic": "HASEHKHHXXX",
          "country": "HK",
          "accountMask": "\\024-000000000[000]",
          "accountPlaceholder": "024-000000000000",
          "accountNotice": ""
        }
      ]
    }
  ],
  "timeNow": 1771501763963
}
```

Sample is truncated for documentation; production response contains additional countries/banks in the same format.

#### 6.1.2 Get bank by IBAN

**1. Description** Resolves bank details from an IBAN.

**2. Request**

| Item            | Value                         | Note |
| --------------- | ----------------------------- | ---- |
| **HTTP Method** | `GET`                         |      |
| **URI**         | `/api/v1/banks/iban/{ibanNo}` |      |
| **Auth Level**  | **No Auth**                   |      |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "",
  "result": {
    "name": "Zürcher Kantonalbank",
    "bankCode": "00700",
    "bankCodes": ["00700", "00730", "00754", "00755", "30700"],
    "bic": "ZKBKCHZZXXX",
    "country": "CH",
    "accountMask": "CH00\\0\\07\\0\\0************",
    "accountPlaceholder": "CH0000700000000000000",
    "accountNotice": ""
  },
  "timeNow": 1771501763963
}
```

#### 6.1.3 Get supported recipient countries and cities

**1. Description** Returns supported recipient country/city combinations for transfer setup.

**2. Request**

| Item            | Value                    | Note |
| --------------- | ------------------------ | ---- |
| **HTTP Method** | `GET`                    |      |
| **URI**         | `/api/v1/country-cities` |      |
| **Auth Level**  | **No Auth**              |      |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "",
  "result": [
    {
      "name": "Austria",
      "countryCode": {
        "iso2": "AT",
        "iso3": "AUT"
      },
      "cities": ["Vienna", "Graz", "Linz"],
      "zipCodeRegEx": "^\\d{4}$"
    },
    {
      "name": "Hong Kong",
      "countryCode": {
        "iso2": "HK",
        "iso3": "HKG"
      },
      "cities": ["Hong Kong"],
      "zipCodeRegEx": "^\\d{3}$"
    }
  ],
  "timeNow": 1771502369957
}
```

Sample is truncated for documentation; production response contains more countries/cities in the same format.

#### 6.1.4 Get payment purpose list

**1. Description** Retrieves supported payment purpose options used for compliance validation before transfer.

**2. Request**

| Item            | Value                      | Note |
| --------------- | -------------------------- | ---- |
| **HTTP Method** | `GET`                      |      |
| **URI**         | `/api/v1/payment-purposes` |      |
| **Auth Level**  | **No Auth**                |      |

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "purposes": [
      {
        "value": 0,
        "name": "Transfer to own account (other bank)"
      },
      {
        "value": 1,
        "name": "Purchase of goods"
      },
      {
        "value": 2,
        "name": "Payment for services (leisure, medical, travel, education, insurance, telecom, etc.)"
      },
      {
        "value": 3,
        "name": "Family support and inheritance"
      },
      {
        "value": 4,
        "name": "Charity donation"
      },
      {
        "value": 5,
        "name": "Salary, Benefits, Dividends"
      },
      {
        "value": 6,
        "name": "Real Estate and rent"
      },
      {
        "value": 7,
        "name": "Credit / debit card coverage"
      },
      {
        "value": 8,
        "name": "Investment, securities, trading"
      },
      {
        "value": 9,
        "name": "Currency exchange"
      },
      {
        "value": 10,
        "name": "Tax and governmental payments"
      },
      {
        "value": 11,
        "name": "Loan, Collateral"
      }
    ]
  },
  "timeNow": 1703123456789
}
```

#### 6.1.5 Verify reference

**1. Description** Verifies user-entered payment reference text and returns `purposeId` and `ref`.

**2. Request**

| Item            | Value                      | Note                      |
| --------------- | -------------------------- | ------------------------- |
| **HTTP Method** | `POST`                     |                           |
| **URI**         | `/api/v1/verify-reference` |                           |
| **Auth Level**  | **Full Auth**              | Requires wallet signature |
| **Headers**     | `Content-Type`             | Fixed: `application/json` |
|                 | `tokenId`                  | User's URID               |
|                 | `network`                  | Network Identifier        |
|                 | `sign`                     | Wallet Signature          |
|                 | `hash`                     | Original request hash     |
|                 | `deadline`                 | Signature deadline        |

**Request Parameters**

```json
{
  "reference": "Invoice 2026-001"
}
```

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "clientPayoutRefParams": {
      "purposeId": 8,
      "refId": "REF-7A6C2A8E"
    }
  },
  "timeNow": 1703123456789
}
```

#### 6.1.6 Verify contact (bank payment request)

**1. Description** Validates final recipient and bank payload (`bankPaymentRequest`) and returns `contactId`, `purposeId`, and `refId`.

**2. Request**

| Item            | Value                    | Note                      |
| --------------- | ------------------------ | ------------------------- |
| **HTTP Method** | `POST`                   |                           |
| **URI**         | `/api/v1/verify-contact` |                           |
| **Auth Level**  | **Full Auth**            | Requires wallet signature |
| **Headers**     | `Content-Type`           | Fixed: `application/json` |
|                 | `tokenId`                | User's URID               |
|                 | `network`                | Network Identifier        |
|                 | `sign`                   | Wallet Signature          |
|                 | `hash`                   | Original request hash     |
|                 | `deadline`               | Signature deadline        |

**Request Parameters**

```json
{
  "account": "CH93 0076 2011 6238 5295 7",
  "bankName": "Hypothekarbank Lenzburg AG",
  "bic": "HYPCH22",
  "purpose": 1,
  "reference": "Invoice 2026-001",
  "creditor": {
    "name": "Alice Doe",
    "street": "Bahnhofstrasse 1",
    "city": "Zurich",
    "zip": "8001",
    "country": "CH"
  }
}
```

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "account": "CH93 0076 2011 6238 5295 7",
    "bankName": "Hypothekarbank Lenzburg AG",
    "bic": "HYPCH22",
    "purpose": 1,
    "creditor": {
      "name": "Alice Doe",
      "street": "Bahnhofstrasse 1",
      "city": "Zurich",
      "zip": "8001",
      "country": "CH"
    },
    "clientPayoutRefParams": {
      "contactId": "SP",
      "purposeId": 1,
      "refId": "REF-7A6C2A8E"
    }
  },
  "timeNow": 1703123456789
}
```

### 6.2 Permit & transfer

#### 6.2.1 Get fees

**1. Description** Returns configured payout fees and minimum payout amounts by currency.

**2. Request**

| Item            | Value                       | Note |
| --------------- | --------------------------- | ---- |
| **HTTP Method** | `GET`                       |      |
| **URI**         | `/api/v1/banks/payout/fees` |      |
| **Auth Level**  | **No Auth**                 |      |

**Request Parameters**

No request body.

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "",
  "result": {
    "EUR": {
      "tokenAddress": "0x0578be9C858e6562dd8cd11a738b89Ca48194dA5",
      "currency": "EUR",
      "fee": "0",
      "minimalPayoutAmount": "1000"
    },
    "CHF": {
      "tokenAddress": "0x53587A05ccDdCE555C2Cd7cE4C9c5Bc3D912E2f3",
      "currency": "CHF",
      "fee": "0",
      "minimalPayoutAmount": "1000"
    },
    "USD": {
      "tokenAddress": "0xD598839598bBF508b97697b7D9e80054D4bcaaCC",
      "currency": "USD",
      "fee": "5000",
      "minimalPayoutAmount": "10000"
    }
  },
  "timeNow": 1771832199786
}
```

#### 6.2.2 Create payout permit request

**1. Description** Creates payout transfer request with user wallet signature (permit-style authorization) and submits on-chain payout.

**2. Request**

| Item            | Value                        | Note                      |
| --------------- | ---------------------------- | ------------------------- |
| **HTTP Method** | `POST`                       |                           |
| **URI**         | `/api/v1/payout-with-permit` |                           |
| **Auth Level**  | **Full Auth**                | Requires wallet signature |

**Request Parameters**

```json
{
  "amount": "25000",
  "permitAmount": "25000",
  "permitDeadline": 1703123456,
  "permitV": 27,
  "permitR": "0x...",
  "permitS": "0x...",
  "contactId": "EA-00017418",
  "tokenAddress": "0x5E52c8993283023B83e87eF577f7f51Fa1c5B007",
  "purposeId": "P001",
  "ref": "REF-7A6C2A8E",
  "metadata": {
    "bankAccountHolder": "Alice Doe",
    "bankName": "Hypothekarbank Lenzburg AG",
    "bankAccount": "CH9300762011623852957",
    "bankReference": "Invoice 2026-001"
  }
}
```

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "txHash": "0xabc123def456..."
  },
  "timeNow": 1703123456789
}
```

**4. Notes**

* Recipient name/address/reference fields should use Latin characters.
* Minimum input for USD payouts is typically 50 USD.
* Gas/network fee is deducted from the user's fiat transfer amount.

**6. Representative Error Conditions**

| Type                   | Description                                  |
| ---------------------- | -------------------------------------------- |
| Signature Error        | User signature verification failed.          |
| Monthly Limit Exceeded | Transfer exceeds monthly quota.              |
| Insufficient Balance   | Available fiat balance is insufficient.      |
| Minimum Amount Error   | Transfer amount below minimum allowed input. |

## 7. FX

This section describes the recommended FX execution flow in External Wallet Access Mode:

1. Call `POST /api/v1/quote/fx` to get the quoted `outputAmount` and `exchangeRate`.
2. Use the quote result to build `amountOutMinimum`.
3. Call `POST /api/v1/fx-exchange-with-permit` to execute the on-chain exchange.

**Amount limits.** Read the minimum and maximum FX amount for each token from the chain config fields `minFxAmount` and `maxFxAmount`; see [Get Supported Chain Config](#id-3.1.9-get-supported-chain-config). Read them at request time and do not hardcode them. UR converts the minimum from a USD anchor at the live rate. The maximum reflects the user's remaining rolling 30-day allowance, so it changes over time.

### 7.1 FX API

#### 7.1.1 Get output amount and exchange rate

**1. Description**\
Returns quoted FX output amount (minimal unit) and exchange rate for a token pair.

**2. Request**

| Item            | Value              | Note                                                |
| --------------- | ------------------ | --------------------------------------------------- |
| **HTTP Method** | `POST`             |                                                     |
| **URI**         | `/api/v1/quote/fx` |                                                     |
| **Auth Level**  | **Basic Auth**     | Requires `tokenId` header                           |
| **Headers**     | `Content-Type`     | Fixed: `application/json`                           |
|                 | `tokenId`          | User's URID                                         |
|                 | `network`          | Network Identifier (`5000` Mainnet, `5003` Testnet) |

**Request Parameters**

```json
{
  "inputAmount": "50000",
  "inputToken": "0xdf79470986629ae4893BfCE0c6C0F4d085E99741",
  "outputToken": "0x5E52c8993283023B83e87eF577f7f51Fa1c5B007"
}
```

* You can get the fiat token addresses from here: [token contract addresses](/api-reference/smart-contracts#contract-addresses-1)

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "",
  "result": {
    "inputAmount": "50000",
    "inputToken": "0xdf79470986629ae4893BfCE0c6C0F4d085E99741",
    "outputToken": "0x5E52c8993283023B83e87eF577f7f51Fa1c5B007",
    "outputAmount": "41869",
    "exchangeRate": "0.83"
  },
  "timeNow": 1772077859153
}
```

**4. Field Notes**

* `inputAmount` / `outputAmount`: minimal unit strings (do not treat as float).
* `exchangeRate`: `1 inputToken = x outputToken`, fixed 2 decimals, truncated down.
* Current quote mode is `exact_in` (input is fixed).

**5. Request Example**

```bash
curl -X POST "https://api.ur.app/api/v1/quote/fx" \
-H "Content-Type: application/json" \
-H "tokenId: 12345" \
-H "network: 5000" \
-d '{
  "inputAmount": "50000",
  "inputToken": "0xdf79470986629ae4893BfCE0c6C0F4d085E99741",
  "outputToken": "0x5E52c8993283023B83e87eF577f7f51Fa1c5B007"
}'
```

#### 7.1.2 Exchange with permit

**1. Description**\
Executes FX on-chain using EIP-2612 style permit signature.

**2. Request**

| Item            | Value                             | Note                                    |
| --------------- | --------------------------------- | --------------------------------------- |
| **HTTP Method** | `POST`                            |                                         |
| **URI**         | `/api/v1/fx-exchange-with-permit` |                                         |
| **Auth Level**  | **Full Auth**                     | Requires signed headers + permit fields |
| **Headers**     | `tokenId`                         | User's URID                             |
|                 | `network`                         | Network Identifier                      |
|                 | `sign`                            | Wallet signature header                 |
|                 | `hash`                            | Request hash header                     |
|                 | `deadline`                        | Header signature deadline               |

**Request Parameters**

```json
{
  "userAddress": "0x1234567890abcdef1234567890abcdef12345678",
  "inputToken": "0xdf79470986629ae4893BfCE0c6C0F4d085E99741",
  "outputToken": "0x5E52c8993283023B83e87eF577f7f51Fa1c5B007",
  "inputAmount": "50000",
  "amountOutMinimum": "41869",
  "permitValue": "50000",
  "permitDeadline": 1772081459,
  "permitV": 27,
  "permitR": "0x...",
  "permitS": "0x..."
}
```

**3. Response**

```json
{
  "retCode": 0,
  "retMsg": "success",
  "result": {
    "txHash": "0xabc123def456..."
  },
  "timeNow": 1703123456789
}
```

**4. Parameter Notes**

* `inputAmount`: exact-in amount (minimal unit).
* `amountOutMinimum`: minimum acceptable output (minimal unit), usually derived from quote `outputAmount`.
* `permitValue`: must be `>= inputAmount`.
* `permitDeadline`: must be in the future.
* `permitR` / `permitS`: 32-byte hex strings (`0x` + 64 hex chars).

#### 7.1.3 Recommended end-to-end flow

1. Call `POST /api/v1/quote/fx` with `inputAmount`, `inputToken`, `outputToken`.
2. Read `result.outputAmount` and `result.exchangeRate`.
3. Decide `amountOutMinimum` policy:
   * conservative: apply a safety factor on quote output;
   * strict: use quote `outputAmount` directly.
4. Build permit signature for `inputToken`.
5. Call `POST /api/v1/fx-exchange-with-permit`.
6. Track final transaction status via `txHash`.

#### 7.1.4 Common error codes (FX related)

| Code    | Type            | Description                                             |
| ------- | --------------- | ------------------------------------------------------- |
| `10002` | Parse Error     | Missing/malformed parameters or headers                 |
| `10009` | Invalid Param   | Invalid token address, amount format, or token pair     |
| `10040` | Quote Try Again | Quote failed due to pricing/rpc conditions; retry quote |
| `10000` | System Error    | Permit verification or on-chain submission failed       |

## 8. OPEN API

This section details the UR-OPEN-API, designed for third-party integrations (B-side). For complete API documentation including authentication, endpoints, and data structures, please refer to the [Open API Reference](/api-reference).

### 8.1 MINT

See [Mint URID](/api-reference#mint-urid) in the Open API Reference.

### 8.2 Get user profile

See [Fetch UR Account information](/api-reference#fetch-ur-account-information) in the Open API Reference.

### 8.3 Get user balance

See [Fetch UR Account balance](/api-reference#fetch-ur-account-balance) in open API Reference.

### 8.4 Transaction query

See [Fetch transaction details](/api-reference#fetch-transaction-details) and [Fetch transaction history](/api-reference#fetch-transaction-history) in the Open API Reference.

### 8.5 Webhook notifications

Webhook definitions are centralized in [Webhooks](https://docs.ur.app/developer-resources/webhook).\
For off-ramp related events, see `transaction` and `allowance`.


# Delegated Contract Mode

Complete API reference for partners integrating via Delegated Contract Mode.

{% hint style="warning" %}
**Will be deprecated soon.** This API reference is maintained for existing partners on Delegated Mode. New partners should integrate via [Managed Custody Mode](/api-reference/account/managed-custody-mode).
{% endhint %}

## API configurations

### Base URLs

The API is deployed across different environments. Use the appropriate base URL for your integration stage.

| Environment    | Base URL                          |
| -------------- | --------------------------------- |
| **Testnet**    | `https://uropenapi-qa.ur-inc.xyz` |
| **Production** | `https://openapi.ur.app`          |

### API authentication

The authentication method for the following Core Banking APIs refers to [this document](/api-reference/signature-and-verify#part-a-partner-authentication-ur-open-api-and-webhooks).

## **User onboarding process**

### **Authorization URL construction**

Partner **directly constructs** the authorization URL without needing to call an API beforehand. When users click the link, they will enter the UR authorization page to complete identity verification and authorization.

**Authorization URL Format**

```
https://get.ur.app/auth/authorize?partner_id={partner_id}&redirect_uri={callback_url}&scope={scopes}&state={custom_state}&response_type=code
```

**URL Parameter Description**

| Parameter       | Required | Description                                                                                                                                                        | Example                                                                                                              |
| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `partner_id`    | Yes      | ID generated by UR for the Partner                                                                                                                                 | `""`                                                                                                                 |
| `redirect_uri`  | Yes      | Callback address after authorization completion, must be pre-registered with UR                                                                                    | `https://partner.com/callback`                                                                                       |
| `scope`         | No       | Requested permission scopes, multiple scopes separated by spaces (URL encoding required)                                                                           | `allowance.mantle.USDC%20allowance.mantle.USD24`                                                                     |
| `state`         | No       | Partner custom state parameter for CSRF attack prevention and business information tracking. UR doesn't parse this value, only returns it as-is during redirection | `A random string you provide to verify against CSRF attacks. The length of this string can be up to 500 characters.` |
| `response_type` | Yes      | Fixed value: `code` (authorization code mode)                                                                                                                      | `code`                                                                                                               |

**Key Design**

* **No need to call API beforehand**: Partner directly constructs URL, reducing integration complexity
* **partner\_id**: UR identifies Partner through `partner_id` and confirms identity through signature when verifying code later
* **state is completely customizable**: Random string to prevent CSRF
* **Similar to standard authorization flow**: Developers don't need to learn a new authorization pattern

### **Available scopes**

| Scope                    | Description                        |
| ------------------------ | ---------------------------------- |
| `allowance.mantle.USDC`  | USDC authorization on Mantle chain |
| `allowance.mantle.USD24` | USD authorization on Mantle chain  |
| `allowance.mantle.EUR24` | EUR authorization on Mantle chain  |
| `allowance.mantle.CHF24` | CHF authorization on Mantle chain  |
| `allowance.mantle.CNH24` | CNH authorization on Mantle chain  |
| `allowance.mantle.SGD24` | SGD authorization on Mantle chain  |
| `allowance.mantle.HKD24` | HKD authorization on Mantle chain  |
| `allowance.mantle.JPY24` | JPY authorization on Mantle chain  |
| `allowance.bsc.USDC`     | USDC authorization on BSC chain    |
| `card`                   | Card issuance                      |

### **User completes authorization**

After the user clicks the authorization link, they will enter the UR authorization page to complete KYC and authorization operations. After authorization is complete, UR will redirect back to the Partner's `redirect_uri` with the authorization code `code` and `state` parameters.

**Redirect URL Format**

```
{redirect_uri}?code={authorization_code}&state={original_state}
```

**Parameter Description**

| Parameter | Description                                                                                                     | Example                    |
| --------- | --------------------------------------------------------------------------------------------------------------- | -------------------------- |
| `code`    | Authorization code, Partner uses this code to exchange for user information (one-time use, valid for 5 minutes) | `UAC_a1b2c3d4e5f6g7h8i9j0` |
| `state`   | Returns the `state` parameter provided by Partner in step one as-is                                             | `""`                       |

**Redirect Example**

Assuming the Partner set `redirect_uri` to `https://partner.example.com/oauth/callback` in step one, after the user completes authorization, the browser will redirect to:

```
https://partner.example.com/oauth/callback?code=UAC_a1b2c3d4e5f6g7h8i9j0&state={state}
```

**Partner Callback Handling Example**

```javascript
// Partner's callback endpoint GET /oauth/callback
app.get('/oauth/callback', (req, res) => {
  const { code, state } = req.query;

  // 1. Verify state parameter to prevent CSRF attacks
  const stateData = JSON.parse(decodeURIComponent(state));
  console.log("Business information:", stateData);
  // { sourceId: 'mobile-app', campaign: 'spring-2024', userId: 'internal-user-12345' }

  // 2. Use code to call UR API to exchange for user information (see step three)
  // ...

  res.send("Authorization successful, processing...");
});
```

### **Partner to fetch user onboarding information**

After receiving the authorization code `code`, Partner calls the UR API to exchange for the user's UR identity and authorization information. This step requires **Ethereum signature verification of Partner identity**. For detailed signing and verification rules, please refer to [Signature and Verification](/api-reference/signature-and-verify).

**Field Description**:

* `userId`: UR user ID
* `userAddress`: User's Ethereum address
* `scope`: Granted permission scopes
* `state`: Returns the state parameter provided by Partner as-is
* `authorizedAt`: Authorization completion timestamp (Unix timestamp)

**Error Response**:

```json
{
  "code": 4001,
  "message": "Invalid or expired authorization code",
  "data": null
}
```

### Public data structures

#### TransactionData

Transaction data structure used in `/v1/transactions`, `/v1/transaction/query`, and `webhook.event.transaction`.

**Complete Example** (based on actual API response):

```json
{
  "urId": "7639951412",
  "title": "usd",
  "subtitle": "eip155:5003 USDC 12",
  "amount": "+11.94",
  "type": "CTU",
  "timestamp": 1768659501,
  "image": "",
  "currency": "usd",
  "direction": "IN",
  "txHash": "0x21b4dfa7be02b4e806cf8bc5469ef6af35c76afa6fbace2826c9f8afb2e06cc6",
  "chainId": "eip155:5003",
  "inputToken": "USDC",
  "inputAmount": "12",
  "inputTokenAddress": "0xe6a2802837da44f880f52c1681b6740db208755c",
  "outputAmount": "",
  "mcc": 0,
  "reference": "",
  "status": "",
  "bankAccount": "",
  "crdMultiToken": null,
  "ctuExternalSender": "0x96e023fb9b446a65273a0c01cdafdf26b5f21b3f",
  "token": "",
  "tokenAddress": "",
  "fromAddress": "",
  "toAddress": "",
  "statusCode": "",
  "crdCurrency": "",
  "listingTitle": "",
  "txHashUrl": "https://sepolia.mantlescan.xyz/tx/0x21b4dfa7be02b4e806cf8bc5469ef6af35c76afa6fbace2826c9f8afb2e06cc6",
  "txIdIcon": "",
  "officialName": "",
  "partnerRefId": ""
}
```

**Field Descriptions**:

| Field               | Type   | Required | Description                                                                                                                                                                                                                                                     |
| ------------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `urId`              | string | Yes      | UR user ID (string format)                                                                                                                                                                                                                                      |
| `title`             | string | Yes      | Transaction title (for display)                                                                                                                                                                                                                                 |
| `subtitle`          | string | Yes      | Transaction subtitle (e.g., counterparty UR ID, merchant name, chain info, etc.)                                                                                                                                                                                |
| `amount`            | string | Yes      | Transaction amount (**display format with decimals**, e.g., `"+11.94"`), with sign indicating direction                                                                                                                                                         |
| `type`              | string | Yes      | Transaction type abbreviation: CTU (crypto top-up / crypto deposit), FRX (forex exchange), P2P (peer-to-peer transfer), CTF (crypto transfer), CSW (crypto swap), ONR (onramp), CRD (card payment), CDP (cash deposit), CWD (cash payout / withdrawal), UNKNOWN |
| `timestamp`         | int64  | Yes      | Transaction timestamp (Unix seconds)                                                                                                                                                                                                                            |
| `image`             | string | No       | Transaction display icon URL or card logo                                                                                                                                                                                                                       |
| `currency`          | string | Yes      | Currency identifier (lowercase format, e.g., `"usd"`, `"eur"`)                                                                                                                                                                                                  |
| `direction`         | string | Yes      | Transaction direction: `IN` (credit) / `OUT` (debit)                                                                                                                                                                                                            |
| `txHash`            | string | Yes      | On-chain transaction hash (0x prefix)                                                                                                                                                                                                                           |
| `chainId`           | string | No       | Chain ID (CAIP-2 format, e.g., `eip155:5003` for Mantle Sepolia testnet). Wire key is `chainId`.                                                                                                                                                                |
| `inputToken`        | string | No       | Input token symbol                                                                                                                                                                                                                                              |
| `inputAmount`       | string | No       | Input amount (**integer string** in smallest on-chain unit, e.g., `"12"`)                                                                                                                                                                                       |
| `inputTokenAddress` | string | No       | Input token contract address (lowercase with 0x prefix)                                                                                                                                                                                                         |
| `outputAmount`      | string | No       | Output amount (for exchange transactions, smallest on-chain unit)                                                                                                                                                                                               |
| `mcc`               | uint64 | No       | Merchant category code (card payment transactions only)                                                                                                                                                                                                         |
| `reference`         | string | No       | Transaction reference number                                                                                                                                                                                                                                    |
| `status`            | string | No       | Transaction status: pending/completed/rejected/unknown. Empty string if status is unknown or not applicable                                                                                                                                                     |
| `bankAccount`       | string | No       | Bank account information                                                                                                                                                                                                                                        |
| `crdMultiToken`     | array  | No       | Card payment multi-token deduction details (array of `{token: string, amount: string}`)                                                                                                                                                                         |
| `ctuExternalSender` | string | No       | External sender address for cross-chain transfer-in transactions (lowercase with 0x prefix)                                                                                                                                                                     |
| `token`             | string | No       | Token symbol                                                                                                                                                                                                                                                    |
| `tokenAddress`      | string | No       | Token contract address                                                                                                                                                                                                                                          |
| `fromAddress`       | string | No       | Sender address                                                                                                                                                                                                                                                  |
| `toAddress`         | string | No       | Recipient address                                                                                                                                                                                                                                               |
| `statusCode`        | string | No       | Detailed status code                                                                                                                                                                                                                                            |
| `crdCurrency`       | string | No       | Card payment original currency                                                                                                                                                                                                                                  |
| `listingTitle`      | string | No       | Display title                                                                                                                                                                                                                                                   |
| `txHashUrl`         | string | No       | Blockchain explorer link                                                                                                                                                                                                                                        |
| `txIdIcon`          | string | No       | Transaction icon URL                                                                                                                                                                                                                                            |
| `officialName`      | string | No       | Official name                                                                                                                                                                                                                                                   |
| `partnerRefId`      | string | No       | Partner custom reference ID (e.g., partner's order number)                                                                                                                                                                                                      |

**Key Field Format Notes**:

* **`urId`**: Always returned as a string, not a number
* **`amount`**: Display format with sign and decimals (e.g., `"+11.94"`, `"-6.00"`)
* **`currency`**: Lowercase format (e.g., `"usd"`, `"eur"`, not `"USD"` or `"EUR"`)
* **`inputAmount`**: Integer string representing smallest on-chain unit (e.g., `"12"` for USDC means 0.000012 USDC with 6 decimals)
* **`txHashUrl`**: Complete blockchain explorer URL for the transaction
* **Optional fields**: May be empty string or omitted depending on transaction type

#### **Chain ID reference**

The `chainId` field in API requests uses the CAIP-2 standard format `eip155:<chainId>`, supporting the following chains:

**Mantle Chain**:

* **Testnet**: `eip155:5003` (Mantle Sepolia Testnet)
* **Mainnet**: `eip155:5000` (Mantle Mainnet)

**BSC Chain**:

* **Testnet**: `eip155:97` (BSC Testnet)
* **Mainnet**: `eip155:56` (BSC Mainnet)

> **Important Notes**:
>
> * If `chainId` is not specified in the request, it defaults to Mantle chain
> * Cross-chain deposits (e.g., from BSC) require explicit `chainId` and `userAddress` parameters
> * CAIP-2 format example: `eip155:97` represents BSC Testnet (chain ID 97)

#### **Unified amount field description**

**Unified amount field description**: All amount-related request fields named `amount`, `minAmount`, `maxAmount`, `minUsdcOut`, `feeAmount`, `minAmountOut`, etc., use the **string** type and are converted to the smallest on-chain unit based on the token's `decimals` (integers only). For example, for USD (`decimals=2`), 5.00 should be sent as `"500"`; for USDC (`decimals=6`), 1.23 should be sent as `"1230000"`.

## Core banking APIs

### **Mint UR NFT**

Refers to [this open API](/api-reference#mint-urid).

### **Card**

Webview URL: `https://get.ur.app/partner-login?partnerId=[Your PartnerId]&feature=card`

By opening this URL, users can activate, view, and manage their card.

### **Profile**

Webview Url: `https://get.ur.app/partner-login?partnerId=[Your PartnerId]&feature=profile`

By logging in to this URL, users can view their personal information, including Name, IBAN, Address, Bank Name, and BIC/SWIFT.

![profile-demo](/files/Ty63Bm5z67sYpKZIziZq)

### **Bank transfer**

Webview URL: `https://get.ur.app/partner-login?partnerId=[Your PartnerId]&feature=bank-transfer`

By opening this URL, users can initiate and manage bank transfers, including selecting the source account, entering the recipient's bank details, specifying the amount, and reviewing transfer history.

### **Fetch UR Account information**

Refers to [this open API](/api-reference#fetch-ur-account-information).

### Fetch UR Account balance

Refers to [this open API](/api-reference#fetch-ur-account-balance).

### Get supported asset list

**Endpoint: GET /v1/tokens**

Partners can fetch the token list currently configured by UR for this partner (including on-chain precision and display precision), synchronized in real time with on-chain `chaincontractconfig`.

**Request Parameters**: None (GET request, no request body required).

**Response Example**:

```json
{
  "code": 0,
  "message": "",
  "data": [
    {
      "symbol": "USD",
      "name": "USD24",
      "address": "0xdf79470986629ae4893BfCE0c6C0F4d085E99741",
      "decimals": 2,
      "displayDecimals": 2,
      "isFiat": true
    },
    {
      "symbol": "USDC",
      "name": "USD Coin",
      "address": "0xe6a2802837da44F880f52c1681b6740db208755C",
      "decimals": 6,
      "displayDecimals": 2,
      "isFiat": false
    }
  ]
}
```

**Response Field Description**:

* `symbol`: Token symbol
* `name`: Token full name
* `address`: Token contract address
* `decimals`: On-chain smallest unit precision (`10^decimals`)
* `displayDecimals`: Recommended frontend display precision (can differ from `decimals`, e.g., 6 on-chain, 2 for display)
* `isFiat`: Whether it's a fiat token
* Token order follows on-chain configured `priority`, alphabetically by symbol if priority is the same.

### Fetch currency exchange rate

**POST /v1/exchangeRate**

Get the exchange rate for the given asset pair.

**Request Parameters**:

```json
{
  "inSymbol": "EUR",
  "outSymbol": "USD"
}
```

| Field       | Type   | Required | Description  |
| ----------- | ------ | -------- | ------------ |
| `inSymbol`  | string | Yes      | Input token  |
| `outSymbol` | string | Yes      | Output token |

**Response Example**:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "rate": "0.998",
    "exchangeSpread": "0.002"
  }
}
```

**Response Field Description**:

* `rate`: Exchange rate
* `exchangeSpread`: Exchange rate spread

### **Currency exchange**

**POST /v1/exchange**

User exchange endpoint.

**Request Parameters**:

```json
{
  "urId": 12345,
  "inputToken": "USD",
  "outputToken": "EUR",
  "amount": "10000",
  "requestId": "req-20240101-0001"
}
```

| Field         | Type   | Required | Description                                                                  |
| ------------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `urId`        | int64  | Yes      | UR user ID                                                                   |
| `inputToken`  | string | Yes      | Input token, use `symbol` returned by `/v1/tokens`                           |
| `outputToken` | string | Yes      | Output token, use `symbol` returned by `/v1/tokens`                          |
| `amount`      | string | Yes      | Exchange amount, on-chain smallest unit string (e.g., 100.00 USD => `10000`) |
| `requestId`   | string | Yes      | Request idempotency ID (client-defined, for idempotency and troubleshooting) |

**Response Example**:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "txHash": "0x1234567890abcdef..."
  }
}
```

**Response Field Description**:

* `txHash`: Transaction hash

### Fetch transaction history

Refers to [this open API](/api-reference#fetch-transaction-history).

### Fetch offramp quote

**POST /v1/deposit/quote**

Returns fees, exchange rates, and estimated receipt amount for Fiat deposit scenarios. The current version supports direct deposits of Mantle-chain `USDC` to fiat tokens (`USD`, `EUR`, `CHF`, etc.).

**Request Parameters**:

```json
{
  "urId": 12345,
  "fromToken": "USDC",
  "toToken": "USD",
  "amount": "5000000",
  "chainId": "eip155:97",
  "userAddress": "0x1234567890abcdef..."
}
```

| Field         | Type   | Required | Description                                                                                                                                       |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `urId`        | int64  | Yes      | UR user ID                                                                                                                                        |
| `fromToken`   | string | Yes      | Input token symbol (currently only supports `USDC`), available `symbol` can be obtained from `/v1/tokens`                                         |
| `toToken`     | string | Yes      | Target tiat token (`USD`, `EUR`, etc.), use `symbol` returned by `/v1/tokens`                                                                     |
| `amount`      | string | Yes      | Input amount in smallest on-chain unit (USDC has 6 decimals: `5000000` = 5 USDC). **Minimum: 5 USDC**                                             |
| `chainId`     | string | No       | Source chain ID in CAIP-2 format (e.g., `eip155:5003` for Mantle Sepolia, `eip155:97` for BSC Testnet). Defaults to Mantle chain if not specified |
| `userAddress` | string | No       | User's UR account address (required when the user deposits from non-Mantle chains)                                                                |

**Response Example**:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "quoteId": "deposit_1700000000000_12345",
    "feeAmountViaUsdc": "0.50",
    "outputAmount": "999.50",
    "outputAmountBeforeFee": "1000.00",
    "exchangeRate": "1.0",
    "crossChainFee": "0",
    "best": {
      "aggregator": "ur",
      "to": "0x...",
      "swapCalldata": "0x",
      "minUsdcAmount": "990000",
      "expectedUsdcAmount": "1000000",
      "slippageBps": 50,
      "deadline": 1703123600,
      "priceImpact": "0"
    },
    "feeAmountViaNativeToken": "0",
    "processingFee": "0.50",
    "networkFee": "0",
    "totalFee": "0.50",
    "allQuotes": [],
    "chainId": "eip155:5000"
  }
}
```

**Response Field Description**:

| Field                     | Type   | Description                                                                                                                                                          |
| ------------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quoteId`                 | string | Quote ID, pass this to `/v1/deposit` to lock in the quoted fees                                                                                                      |
| `feeAmountViaUsdc`        | string | Network gas fee (USDC denominated, decimal string, e.g., `0.05` means 0.05 USDC); paid by the user, deducted from the user's USDC input.                             |
| `outputAmount`            | string | Final amount user will receive in target token (decimal string, after all fees and spread)                                                                           |
| `exchangeRate`            | string | Exchange rate from input `fromToken` to `toToken`                                                                                                                    |
| `crossChainFee`           | string | Cross-chain LayerZero fee in native token's smallest unit (e.g., wei for ETH, only applicable for non-Mantle chains); paid by the user from the source chain wallet. |
| `feeAmountViaNativeToken` | string | Fee in native token                                                                                                                                                  |
| `processingFee`           | string | Processing fee                                                                                                                                                       |
| `networkFee`              | string | Network fee                                                                                                                                                          |
| `totalFee`                | string | Total fee                                                                                                                                                            |
| `chainId`                 | string | Chain ID                                                                                                                                                             |
| `allQuotes`               | array  | All available quotes. If it is USDC, ignore this field.                                                                                                              |
| `best`                    | object | Best quote details. If it is USDC, ignore this field.                                                                                                                |

> **Important Notes**:
>
> * `amount` must be at least **5 USDC** (5000000 in smallest unit)
> * The actual output amount deducts multiple fees: network fee (\~0.05 USDC), processing fee (\~0.5% of deposit), and spread adjustment
> * For cross-chain deposits (BSC, etc.), `crossChainFee` shows the LayerZero bridge fee in native token units
> * If `amount` is below minimum or `toToken` is not configured on-chain, error code 20003 will be returned

### Create offramp request

**POST /v1/deposit**

User deposit endpoint.

**Request Parameters**:

```json
{
  "urId": 12345,
  "inputToken": "USDC",
  "outputToken": "USD",
  "amount": "5000000",
  "requestId": "deposit-202401010001",
  "quoteId": "deposit_1700000000000_12345",
  "chainId": "eip155:97",
  "userAddress": "0x1234567890abcdef..."
}
```

| Field         | Type   | Required | Description                                                                                                                                 |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `urId`        | int64  | Yes      | UR user ID                                                                                                                                  |
| `inputToken`  | string | Yes      | Input token (only supports `USDC`), available `symbol` can be obtained from `/v1/tokens`                                                    |
| `outputToken` | string | Yes      | Output token (fiat token), use `symbol` returned by `/v1/tokens`                                                                            |
| `amount`      | string | Yes      | Deposit amount in smallest on-chain unit (e.g., 5 USDC = `5000000`). **Minimum: 5 USDC**                                                    |
| `requestId`   | string | Yes      | Idempotent request ID, must remain the same on retry                                                                                        |
| `quoteId`     | string | Yes      | Quote ID returned by `/v1/deposit/quote`.                                                                                                   |
| `chainId`     | string | No       | Source chain ID in CAIP-2 format (e.g., `eip155:5003` for Mantle Sepolia, `eip155:97` for BSC Testnet). Defaults to Mantle if not specified |
| `userAddress` | string | No       | User's wallet address (required for cross-chain deposits from non-Mantle chains)                                                            |

**Response Example**:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "txHash": "0x1234567890abcdef..."
  }
}
```

**Response Field Description**:

* `txHash`: Transaction hash

> **Important Notes**:
>
> * `requestId` must be globally unique and reused on retry to ensure idempotency
> * `amount` must be at least **5 USDC** (5000000 in smallest unit)
> * Currently supports `USDC → Fiat token` deposits from Mantle and BSC chains
> * For BSC deposits, `userAddress` is required for cross-chain fee calculation
> * **Recommended**: Always call `/v1/deposit/quote` first and pass the returned `quoteId` to lock in the quoted fees. When `quoteId` is provided, the server validates that `urId`, `inputToken`, `outputToken`, and `amount` match the original quote; any mismatch will be rejected. If the quote has expired, request a new one
> * When `quoteId` is omitted, the deposit is processed without fee deduction (backward-compatible behavior).

### **Create onramp request**

{% hint style="warning" %}
**Available soon.** On-ramp (fiat-to-crypto) is not yet available for integration and will be enabled in a future release. The reference below is provided for preview only.
{% endhint %}

**POST /v1/onramp**

Exchanges a fiat token to USDC and (optionally) performs a swap/bridge to the target token based on the quote from `/v1/onramp/quote`.

**Request Parameters**

```json
{
  "quoteId": "onramp_1700000000000000000",
  "requestId": "onramp-demo-001",
  "urId": 7639951412,
  "chainId": "eip155:5000",
  "tokenIn": "USD",
  "amountIn": "10000",
  "withdrawAddress": "0xUserWithdrawAddress",
  "dstChainId": "eip155:8453",
  "dstAggregator": "0xAggregatorAddress",
  "dstTokenOut": "USDC",
  "dstSwapCalldata": "0xabcdef...",
  "dstMinAmountOut": "99000000"
}
```

| Field             | Type   | Required | Description                                                                           |
| ----------------- | ------ | -------- | ------------------------------------------------------------------------------------- |
| `quoteId`         | string | Yes      | Quote ID returned by `/v1/onramp/quote`                                               |
| `requestId`       | string | Yes      | Idempotent request ID, reuse on retry                                                 |
| `urId`            | int64  | Yes      | UR user ID                                                                            |
| `tokenIn`         | string | Yes      | Fiat token symbol, use `symbol` returned by `/v1/tokens`                              |
| `amountIn`        | string | Yes      | Input amount (smallest unit). **After BufferPool quote conversion, must be ≥ 5 USDC** |
| `withdrawAddress` | string | No       | User-specified withdrawal address for onramp                                          |
| `chainId`         | string | No       | Source chain ID, e.g. `eip155:5000` (currently Mantle only)                           |
| `dstChainId`      | string | No       | Destination chain ID for bridge/swap, e.g. `eip155:8453`                              |
| `dstAggregator`   | string | No       | Aggregator contract address, use `best.aggregator` from quote                         |
| `dstTokenOut`     | string | No       | Destination token symbol                                                              |
| `dstSwapCalldata` | string | No       | Swap calldata, use `best.swapCalldata` from quote                                     |
| `dstMinAmountOut` | string | No       | Minimum acceptable USDC amount (smallest unit), use `best.minUsdcAmount` from quote   |

**Response Example**

```json
{
  "code": 0,
  "message": "",
  "data": {
    "txHash": "0x1234567890abcdef..."
  }
}
```

**Description**

* Returns `txHash` on success, transaction details can be queried via `/v1/transactions`.
* `dstAggregator`, `dstSwapCalldata`, `dstMinAmountOut` should come from the latest `/v1/onramp/quote` response.
* If no swap/bridge is needed, omit all `dst*` fields to perform only fiat token → USDC.

### **Fetch onramp quote**

**POST /v1/onramp/quote**

Returns a real-time quote for fiat token → USDC (with optional swap/bridge info) based on BufferPool and limit rules.

**Request Parameters**

```json
{
  "urId": 7639951412,
  "chainId": "eip155:5000",
  "fromToken": "USD",
  "toToken": "USDC",
  "amount": "10000",
  "slippageBps": 50
}
```

| Field         | Type   | Required | Description                                                                           |
| ------------- | ------ | -------- | ------------------------------------------------------------------------------------- |
| `urId`        | int64  | Yes      | UR user ID                                                                            |
| `fromToken`   | string | Yes      | Fiat token symbol, use `symbol` returned by `/v1/tokens`                              |
| `toToken`     | string | Yes      | Destination token symbol, use `symbol` returned by `/v1/tokens`                       |
| `amount`      | string | Yes      | Input amount (smallest unit). **After BufferPool quote conversion, must be ≥ 5 USDC** |
| `slippageBps` | int32  | No       | Slippage in bps, default 50                                                           |
| `chainId`     | string | No       | Chain ID, e.g. `eip155:5000` (currently Mantle only)                                  |

**Response Example**

```json
{
  "code": 0,
  "message": "",
  "data": {
    "quoteId": "onramp_1700000000000000000",
    "chainId": "eip155:5000",
    "best": {
      "aggregator": "0xAggregatorAddress",
      "to": "0xAggregatorTarget",
      "swapCalldata": "0xabcdef...",
      "minUsdcAmount": "99500000",
      "expectedUsdcAmount": "99800000",
      "slippageBps": 50,
      "deadline": 1700000000,
      "priceImpact": "0.0012"
    },
    "allQuotes": [
      {
        "source": "aggregatorA",
        "expectedUsdcAmount": "99800000",
        "priceImpact": "0.0012"
      }
    ],
    "isSupport": true,
    "needLivenessCheck": false,
    "outputAmount": "99800000",
    "exchangeRate": "0.9995",
    "crossChainFee": "0",
    "feeAmountViaUsdc": "500000",
    "feeAmountViaNativeToken": "0",
    "maxAmounts": {
      "USD": "50000",
      "EUR": "46500"
    },
    "livenessCheckUrl": "https://example.com?token=xxx&partnerId=7639951412"
  }
}
```

**Response Field Description**

| Field                     | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                    |
| ------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `quoteId`                 | string | Quote ID                                                                                                                                                                                                                                                                                                                                                                                       |
| `chainId`                 | string | Chain ID                                                                                                                                                                                                                                                                                                                                                                                       |
| `best`                    | object | Best quote details, see `OnrampBestQuote`                                                                                                                                                                                                                                                                                                                                                      |
| `allQuotes`               | array  | Aggregator quotes list, see `OnrampAltQuote`                                                                                                                                                                                                                                                                                                                                                   |
| `feeAmountViaUsdc`        | string | Fee amount denominated in USDC (smallest unit), deducted from the user's input fiat.                                                                                                                                                                                                                                                                                                           |
| `outputAmount`            | string | Final output amount (smallest unit)                                                                                                                                                                                                                                                                                                                                                            |
| `exchangeRate`            | string | Effective exchange rate after fee deduction                                                                                                                                                                                                                                                                                                                                                    |
| `crossChainFee`           | string | Cross-chain fee (smallest unit), deducted from the user's input fiat.                                                                                                                                                                                                                                                                                                                          |
| `feeAmountViaNativeToken` | string | Fee amount denominated in native token (smallest unit), deducted from the user's input fiat.                                                                                                                                                                                                                                                                                                   |
| `isSupport`               | bool   | Whether the current UR ID is allowed to continue onramp (based on liveness status, region restrictions, USDC de-peg checks, etc.)                                                                                                                                                                                                                                                              |
| `needLivenessCheck`       | bool   | **Facial verification trigger**: Indicates whether the current transaction requires a biometric liveness check (for example, a facial scan) before processing. This is dynamically determined by UR's risk engine based on the user's profile and requested onramp amount. If `true`, the frontend must guide the user through the verification flow.                                          |
| `maxAmounts`              | object | The maximum allowable input value for a single transaction. Any input exceeding this threshold will result in a transaction failure. Important: This limit is enforced strictly by risk control rules and is independent of the user's `available` monthly limit. Users may have sufficient monthly allowance but still be restricted by this single-transaction cap due to security policies. |
| `livenessCheckUrl`        | string | Liveness check URL (only when `isSupport && needLivenessCheck`)                                                                                                                                                                                                                                                                                                                                |

**OnrampBestQuote**

| Field                | Type   | Description                                 |
| -------------------- | ------ | ------------------------------------------- |
| `aggregator`         | string | Aggregator name/address                     |
| `to`                 | string | Target contract address                     |
| `swapCalldata`       | string | Swap calldata                               |
| `minUsdcAmount`      | string | Minimum USDC output amount (smallest unit)  |
| `expectedUsdcAmount` | string | Expected USDC output amount (smallest unit) |
| `slippageBps`        | int32  | Slippage in bps                             |
| `deadline`           | int64  | Quote deadline timestamp                    |
| `priceImpact`        | string | Price impact                                |

**OnrampAltQuote**

| Field                | Type   | Description                                 |
| -------------------- | ------ | ------------------------------------------- |
| `source`             | string | Aggregator source                           |
| `expectedUsdcAmount` | string | Expected USDC output amount (smallest unit) |
| `priceImpact`        | string | Price impact                                |

> **Note**: If `isSupport = false`, guide the user to try again later or complete the required liveness verification.

### **Fetch transaction details**

Refers to [this open API](/api-reference#fetch-transaction-details).

## Webhooks

Webhook definitions are centralized in [Webhooks](https://docs.ur.app/developer-resources/webhook). For delegated contract mode, the relevant events are `transaction` and `allowance`.

## Environment

```sepolia
# sepolia
url: 

# auth sepolia
https://ur-fe-tob.qa4.gomantle.org/auth/authorize?state=1&partner_id={partner_id}&redirect_uri={redirect_uri}&response_type=code&scope=allowance.mantle.USDC%20allowance.mantle.USD24%20allowance.mantle.EUR24

# card url
https://ur-fe-tob.qa4.gomantle.org/partner-login?partnerId=90001&feature=card
```

```mainnet
# mainnet
url: https://openapi.ur.app

# auth 
https://get.ur.app/auth/authorize?state=1&partner_id={partner_id}&redirect_uri={redirect_uri}&response_type=code&scope=allowance.mantle.USDC%20allowance.mantle.USD24%20allowance.mantle.EUR24

# card url
https://get.ur.app/partner-login?partnerId={partnerId}&feature=card


```

## Partner configuration required

```
signerAddress: Partner signing publicAddress
webhookUrl: URL to receive Webhook notifications
url: Web redirect URL (partner page linked from UR authorization page)
redirectDomain: Allowed auth redirect domains, multiple values supported for testing

```

> Additional notes:
>
> * Both environments have identical signature/authentication logic, differences only in on-chain data and chain ID, choose Base URL as needed.
> * For QA, staging, or dedicated environments, please contact UR representative to obtain independent domain and public key.

## Frequently asked questions (FAQ)

#### Authorization flow related

**Q: How long is the authorization code valid?**

A: Authorization codes are valid for 5 minutes by default and are single-use. Partners should call `/v1/profile` immediately after receiving the code to exchange for user information.

**Q: What happens if the user denies authorization?**

A: UR will redirect back to `redirect_uri` with an `error` parameter:

```
https://partner.com/callback?error=access_denied&error_description=User+denied+authorization&state=xxx
```

**Q: Must redirect\_uri be pre-registered?**

A: Yes. For security, partners must pre-register all possible `redirect_uri` URLs in the UR system. During authorization, UR will verify if the `redirect_uri` in the URL is on the whitelist.

***

## Error code description

| Error Code | Description                                                                               | Solution                                                                           |
| ---------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| 10001      | Invalid request signature (`ErrSignatureInvalid`)                                         | Check signature algorithm, deadline, and whether private key is correct            |
| 20001      | Request body is empty or missing required fields (`ErrReqBodyEmpty`)                      | Ensure request body is valid JSON and includes required fields                     |
| 20002      | Request body parsing failed (`ErrParseRequestFailed`)                                     | Check JSON format, encoding, and Content-Type                                      |
| 20003      | Invalid parameter (`ErrInvalidParam`), common in `amount` format, token name, urId errors | Adjust parameters based on error message, follow smallest unit string requirements |
| 30001      | NFT already exists (`ErrNFTIdExists`)                                                     | NFT already exists in DB or on-chain, use existing Token ID                        |
| 50001      | Server failed to read request (`ErrIoRead`)                                               | Typically network read/write exception, can retry                                  |
| 50002      | Internal error (`ErrInternal`)                                                            | UR internal service exception, retry later or contact UR support                   |
| 50003      | On-chain or external service exception (`ErrChainRpc`)                                    | Chain RPC or third-party service failure, can retry later                          |


# Managed Custody Mode

Core banking API reference for partners integrating via Managed Custody Mode.

> This document is the Core Banking OpenAPI reference for Partners integrating with UR in **Managed Custody Mode**. Section 3 covers onboarding for Partners whose users complete KYC in the Partner's Sumsub flow and share that verification with UR through Sumsub reuse. Fund-moving APIs still require the user's UR account to be `Live` and the mapped Partner user to exist.
>
> For the conceptual definition of Managed Custody Mode and how it compares to External Wallet Access Mode, see [Integration Guide](https://docs.ur.app/getting-started/integration-guide#id-2-account-mode-how-the-partner-accesses-the-users-ur-account).

***

## 1. Mode context

### 1.1 Where this API sits

Managed Custody Mode is one of UR's two Account Modes. In this mode:

* The user's UR account (URID + tokenized fiat balances) lives inside a **UR-managed embedded wallet**. For compliance, this wallet holds tokenized fiat only; it never custodies the user's crypto, which always sits in an external (non-UR) wallet.
* The Partner backend accesses that account **entirely through REST APIs**, signed with the Partner's signer key.
* Your backend submits routine banking actions (FX, internal transfers, Pay-in, Payout, On-ramp (coming soon), Off-ramp, Card). UR validates each request, runs compliance, risk, and limit checks, and executes on-chain settlement using UR's wallet infrastructure.
* The user is **not prompted** to sign on-chain transactions for routine banking actions.

The Partner owns the entire UX surface; UR is the regulated financial infrastructure underneath.

### 1.2 Operation layer map

Every Core Banking endpoint maps to one of UR's seven core operations. This table is the canonical anchor for the rest of this document.

| Operation                 | Direction                                            | Endpoint family                                                                                                                 | Settlement                                |
| ------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **On-ramp** (coming soon) | User fiat → crypto (delivered to an external wallet) | [§10](#id-10-on-ramp)                                                                                                           | Async via webhook                         |
| **Off-ramp**              | Crypto (from an external wallet) → user fiat         | [§7](#id-7-off-ramp)                                                                                                            | Async via webhook                         |
| **FX**                    | One fiat token → another fiat token                  | [§8](#id-8-fx)                                                                                                                  | On-chain, near-instant; webhook confirms  |
| **Internal transfer**     | One UR Account → another UR Account                  | [§8.3](#id-8.3-initiate-an-internal-transfer)                                                                                   | On-chain; webhook confirms                |
| **Pay-in**                | External bank → user IBAN                            | Covered in [Deposits](https://docs.ur.app/concepts/deposits)                                                                    | Async (SEPA/SWIFT)                        |
| **Payout**                | User fiat → external bank                            | [§9](#id-9-bank-payout)                                                                                                         | Async (SEPA/SWIFT)                        |
| **Card**                  | User → merchant via Mastercard                       | [§11](#id-11-card) (Card-Mode-specific; see [API Reference: Card Mode: Crypto Backed](/api-reference/cards/crypto-backed-card)) | Real-time authorization, async settlement |

Read-only endpoints ([Profile §5](#id-5-profile), [Balance §6](#id-6-balance), [Transactions §12](#id-12-transactions)) sit beside these operations.

***

## 2. API foundation

### 2.1 Base URLs

| Environment    | Base URL                          |
| -------------- | --------------------------------- |
| **Production** | `https://openapi.ur.app`          |
| **Preview**    | `https://openapi-preview.ur.app`  |
| **Testnet**    | `https://uropenapi-qa.ur-inc.xyz` |

> Confirm the exact base URL set with UR before production rollout.

### 2.2 Authentication: Partner Auth (EIP-191)

All authenticated **Partner → UR** requests use **Partner Auth** with EIP-191 signatures.

Every authenticated request must include:

| Header            | Required | Description                                                                                                        |
| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------ |
| `X-Api-Signature` | Yes      | `0x`-prefixed 65-byte hex signature over the Partner Auth message, produced with the Partner signer's private key. |
| `X-Api-Deadline`  | Yes      | Unix seconds. UR rejects the request if `now > deadline`. Recommended validity window ≤ 5 min.                     |
| `X-Api-PublicKey` | Yes      | The Partner signer address (`0x`-prefixed). Must be registered with UR.                                            |

Canonical payload:

* `GET` requests sign the raw query string exactly as sent, without the leading `?`.
* Non-`GET` requests sign the raw request body exactly as sent.
* If the request has no query string or body, use an empty string.

The canonical payload is part of the signed message. `{canonicalPayload}` is not literal text. Replace it with the exact request body or query string that your backend sends.

Build the Partner Auth message from the canonical payload, user identity suffix, and deadline:

```
{canonicalPayload}urId:{X-Ur-Id}externalUserId:{X-External-User-Id} {X-Api-Deadline}
```

If an identity header is not sent, use an empty value in its slot. Do not add a separator before `urId:`. Add one ASCII space before `X-Api-Deadline`.

For example, a `POST` body of `{"amount":"100"}` with `X-Ur-Id: 7123456789`, no `X-External-User-Id`, and `X-Api-Deadline: 1772002211` signs:

```
{"amount":"100"}urId:7123456789externalUserId: 1772002211
```

For a `GET /api/fma/v1/kyc/form-a-info?sessionId=abc123` request with the same headers, sign:

```
sessionId=abc123urId:7123456789externalUserId: 1772002211
```

For the EIP-191 recovery algorithm, see [Signature and Verification](/api-reference/signature-and-verify).

### 2.3 User identity headers

Every **user-scoped** Partner → UR endpoint must identify the user with at least one of the following headers.

| Header               | Description                                                    |
| -------------------- | -------------------------------------------------------------- |
| `X-Ur-Id`            | The user's URID (numeric token ID of the URID NFT).            |
| `X-External-User-Id` | The Partner's own user ID, mapped to a URID during onboarding. |

Rules:

* Send at least one of `X-Ur-Id` or `X-External-User-Id`. Sending neither is invalid.
* Sending both is allowed. When both are present, UR resolves the user by `X-Ur-Id` first.
* User identity **must not** be duplicated in query parameters or request bodies for user-scoped APIs.

Example:

```
X-Api-Signature: 0x<65-byte hex>
X-Api-Deadline: 1772002211
X-Api-PublicKey: 0x<partner signer address>
X-Ur-Id: 7123456789
```

or:

```
X-Api-Signature: 0x<65-byte hex>
X-Api-Deadline: 1772002211
X-Api-PublicKey: 0x<partner signer address>
X-External-User-Id: partner-user-0001
```

### 2.4 Header block references

To avoid repeating long header tables, endpoint sections refer to these named blocks.

* **User-Scoped Partner Auth Headers**: Partner Auth headers ([§2.2](#id-2.2-authentication-partner-auth-eip-191)) + at least one user identity header ([§2.3](#id-2.3-user-identity-headers)). Used for every user-scoped Partner → UR endpoint.
* **Partner-Scoped Partner Auth Headers**: Partner Auth headers ([§2.2](#id-2.2-authentication-partner-auth-eip-191)) only, no user identity. Used for partner-level endpoints not tied to a single user.
* **Public Metadata Headers**: No auth required. Used for fully public reference endpoints (banks, payment purposes, etc.). UR reserves the right to change this access policy.

### 2.5 Standard response envelope

Core Banking APIs use the standard UR OpenAPI response envelope:

```json
{
  "code": 0,
  "message": "",
  "data": {}
}
```

* `code = 0` → success; non-zero → business error (see endpoint-specific tables and the global error code reference).
* `message` → human-readable explanation, may be empty on success.
* `data` → endpoint-specific payload.

### 2.6 Idempotency

Endpoints that move funds (Off-ramp submission, FX, internal transfer, Payout, On-ramp, Onramp retry) accept a Partner-supplied idempotency key:

* The field name is `reqId`.
* Keep `reqId` **stable across retries** of the same logical operation. If a response is lost or times out, query transaction history by `reqId` before retrying. Do not generate a new `reqId` for the same logical operation.
* Webhook delivery is at-least-once; use `data.txHash` or the transaction `id` as the idempotency key on the Partner side.

### 2.7 Preconditions for fund-moving APIs

Before calling Off-ramp, On-ramp, FX, internal transfer, Payout, or Card APIs, the Partner must ensure:

1. The UR account status is **Live**.
2. The mapped Partner user and UR account exist and are not frozen.
3. Any integration-specific approval required by UR has been enabled for your production setup.

***

## 3. Onboarding

{% hint style="warning" %}
**Authoritative reference:** the share-token reuse flow is specified in full, with exact endpoints, request/response fields, and error codes, in [API reference: Shared-token KYC reuse](/api-reference/kyc-and-kyb/shared-token-kyc-reuse). Where this section and that reference differ, the shared-token reference wins.
{% endhint %}

Use this section when your platform verifies the user through your own Sumsub tenant. During onboarding, your platform mints a single-use Sumsub **share token** (scoped to UR's `clientId`) and hands it to UR. UR imports the applicant via Sumsub **Copy Applicant** and runs **data-level validation** over the copied snapshot (it does **not** re-run Sumsub checks against a UR level), creates a URID, provisions the UR-managed wallet, renders and signs Form A, and activates the user's UR Account.

{% hint style="info" %}
**Two onboarding paths are supported:**

* **Sumsub reuse (share token), this page.** Your platform completes KYC in your own Sumsub tenant and shares the approved applicant with UR through Sumsub reuse. Use this when you already operate a Sumsub tenant.
* **Sumsub SDK in your app.** Your backend requests a UR-issued Sumsub access token and the Sumsub SDK in your app runs KYC against UR's Sumsub tenant. See [API reference: Managed Custody SDK KYC](/api-reference/kyc-and-kyb/managed-custody-sdk-kyc). Use this when you do not run a Sumsub tenant.

Both paths produce the same end state and use the same post-onboarding banking APIs documented in this page.
{% endhint %}

### 3.1 Prerequisites

Before you start onboarding users, make sure the following setup is complete:

* Your Partner Auth signing key is registered with UR. See [API signing key](https://docs.ur.app/getting-started/integration-guide#api-signing-key) for how to create it in the API sandbox or register your own address.
* Your platform and UR are configured as Donor / Recipient Partners in Sumsub.
* Your KYC flow presented the required data-sharing declaration and the user agreed, before identity verification. This declaration is mandatory and is the user-facing basis for the Sumsub reuse on this page; see [the required KYC disclosure](https://docs.ur.app/getting-started/integration-guide#kyc-data-sharing-disclosure).
* Your backend can generate a single-use Sumsub share token scoped to UR's Sumsub `clientId` after the applicant is approved.
* Your backend can hand the share token to UR via `POST /api/fma/v1/kyc/reuse-share-token`.
* Your backend can persist `externalUserId`, `sessionId`, `urId`, and `evmAddress` for each user.
* Your backend can receive onboarding webhooks from UR: `fma.kyc.reuse_check.result` (the async handoff verdict) and `fma.account.result` (final activation). After a user is live, `fma.additional_kyc.required` and `fma.additional_kyc.completed` cover ops-initiated retries.

Store the returned identifiers before you show the user that onboarding has started. Your `externalUserId` is your stable user ID. UR maps that value to the returned `urId`.

### 3.2 Identity headers during onboarding

Onboarding uses the same Partner Auth rules as the rest of this page. The identity header changes after UR creates the user's UR Account.

| Phase        | Endpoint                                             | Required identity header | Notes                                                                                          |
| ------------ | ---------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------- |
| Pre-account  | `POST /api/fma/v1/create-account`                    | `X-External-User-Id`     | Do not send `X-Ur-Id` before UR returns the user's `urId`.                                     |
| Post-account | `/api/fma/v1/kyc/*` and `/api/fma/v1/account-status` | `X-Ur-Id`                | Use the `urId` returned by `/create-account`. Sending `X-External-User-Id` as well is allowed. |

For `GET /api/fma/v1/kyc/form-a-info` and `GET /api/fma/v1/account-status`, sign the raw query string exactly as sent. For `POST` endpoints, sign the raw request body exactly as sent. Append the same identity suffix and deadline described in [§2.2](#id-2.2-authentication-partner-auth-eip-191).

### 3.3 Onboarding flow

The onboarding flow starts after the user has completed KYC in your Sumsub workflow and your backend has minted a single-use share token for UR. You create the UR Account, then hand the share token to UR: UR imports the applicant via Copy Applicant and returns a synchronous verdict (`passed` / `incomplete` / `terminal`). `/kyc/check` is a **read** of that verdict; it does not itself trigger the import.

```mermaid
sequenceDiagram
    participant User as User
    participant Partner as Partner backend
    participant UR as UR OpenAPI
    participant SS as Sumsub (Copy Applicant)
    participant Bank as Banking partner

    User->>Partner: Completes KYC in partner app
    Partner->>SS: Mint single-use share token for UR
    Partner->>UR: POST /api/fma/v1/create-account
    UR-->>Partner: sessionId, urId, evmAddress
    Partner->>UR: POST /api/fma/v1/kyc/reuse-share-token
    UR->>SS: Copy Applicant + data-level validation
    UR-->>Partner: status = passed / incomplete / terminal
    opt Read verdict again (optional)
        Partner->>UR: POST /api/fma/v1/kyc/check
        UR-->>Partner: complete=true or missing fields
    end
    Partner->>UR: GET /api/fma/v1/kyc/form-a-info
    UR-->>Partner: Form A text + textHash
    Partner->>User: Display Form A text
    User->>Partner: Consents
    Partner->>UR: POST /api/fma/v1/kyc/sign-form
    Partner->>UR: POST /api/fma/v1/kyc/submit
    UR->>Bank: Submit account activation
    UR-->>Partner: webhook fma.account.result
```

{% stepper %}
{% step %}

### Create the UR Account

Call `POST /api/fma/v1/create-account` with your `X-External-User-Id` and the user's email. For share-token (push-mode) partners, **do not** send `applicantId`; it is rejected. UR creates or reuses the user's URID, provisions the UR-managed wallet, and returns `sessionId`, `urId`, and `evmAddress`.
{% endstep %}

{% step %}

### Hand off the share token

Call `POST /api/fma/v1/kyc/reuse-share-token` with the single-use `shareToken`. UR imports the applicant via Copy Applicant and returns a synchronous verdict: `passed`, `incomplete` (with `missingFields` to remediate and hand off again), or `terminal` (non-remediable eligibility rejection).
{% endstep %}

{% step %}

### Read the verdict (optional)

`POST /api/fma/v1/kyc/check` with the returned `sessionId` re-serves the last handoff verdict as a pure read (`complete=true` when the session can proceed). Use it if you prefer polling to consuming the `fma.kyc.reuse_check.result` webhook; it does not itself trigger the import.
{% endstep %}

{% step %}

### Show Form A

Call `GET /api/fma/v1/kyc/form-a-info?sessionId=...`. Display `data.text` to the user exactly as returned. Store `data.textHash` for the signing call.
{% endstep %}

{% step %}

### Sign Form A

After the user consents, call `POST /api/fma/v1/kyc/sign-form` with `sessionId` and `textHash`. UR signs Form A with the user's UR-managed custodial wallet.
{% endstep %}

{% step %}

### Submit activation

Call `POST /api/fma/v1/kyc/submit`. UR starts the asynchronous bank account activation process. Treat `fma.account.result` or `GET /api/fma/v1/account-status` as the source of truth for the final `Live` state.
{% endstep %}
{% endstepper %}

### 3.4 Create account

Create or reuse the user's UR Account and onboarding session.

| Item    | Value                                       |
| ------- | ------------------------------------------- |
| Method  | `POST`                                      |
| Path    | `/api/fma/v1/create-account`                |
| Headers | Partner Auth headers + `X-External-User-Id` |

Request body:

```json
{
  "email": "user@example.com"
}
```

Request fields:

| Field   | Required | Description               |
| ------- | -------- | ------------------------- |
| `email` | Yes      | The user's email address. |

{% hint style="warning" %}
For share-token (push-mode) partners, **do not** send `applicantId`; the endpoint rejects a non-empty `applicantId` (`20003`). `applicantId` is only for pull-mode partners, which is a different integration. The applicant is conveyed later via the share token in `/kyc/reuse-share-token`, not here.
{% endhint %}

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "sessionId": "5f8e7c9a-1111-2222-3333-444455556666",
    "urId": 7123456789,
    "evmAddress": "0xUSER_UR_EVM_ADDRESS",
    "state": "PartnerDataIngestion",
    "idempotentReplay": false
  }
}
```

Rules:

* Repeated calls with the same `X-External-User-Id` return the existing onboarding session with `idempotentReplay=true`.
* Persist `sessionId`, `urId`, and `evmAddress` before continuing.
* Use `X-Ur-Id` on subsequent onboarding calls.

### 3.5 Check KYC completeness

Re-read the last handoff verdict (from `/kyc/reuse-share-token`) and check whether the session can proceed to Form A. This is a **pure read**: it does not import the applicant or re-run any check; the import happens in `/kyc/reuse-share-token`.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/kyc/check`          |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "sessionId": "5f8e7c9a-1111-2222-3333-444455556666"
}
```

Complete response:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "state": "SignFormA",
    "complete": true
  }
}
```

Incomplete response:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "state": "IdentityVerification",
    "complete": false,
    "missingFields": [
      "registerRequest.profile.annualSalary",
      "registerRequest.id.MRZ2"
    ]
  }
}
```

The handoff verdict is available synchronously from `/kyc/reuse-share-token` and via the `fma.kyc.reuse_check.result` webhook, so you normally do not need to poll. If you do poll, stop after `/kyc/submit` succeeds or when the session reaches a terminal state.

Error handling:

| Code    | Meaning                            | Partner action                                                                                                   |
| ------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `20004` | The KYC snapshot is incomplete.    | Read `missingFields`, have the user complete the missing data, then mint a fresh share token and hand off again. |
| `40001` | Sumsub is temporarily unavailable. | Back off and retry.                                                                                              |

### 3.6 Get Form A

Fetch the exact Form A text that the user must review.

| Item    | Value                                               |
| ------- | --------------------------------------------------- |
| Method  | `GET`                                               |
| Path    | `/api/fma/v1/kyc/form-a-info?sessionId={sessionId}` |
| Headers | User-Scoped Partner Auth Headers                    |

Request body: none.

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "formAVersion": "v1",
    "text": "Form A text rendered by UR...",
    "textHash": "0xabc123..."
  }
}
```

Rules:

* Display `data.text` to the user exactly as returned.
* Pass `data.textHash` unchanged to `/kyc/sign-form`.
* `textHash` is the `0x`-prefixed keccak256 hash of the UTF-8 bytes of `data.text`.
* If this endpoint returns `20007`, the KYC snapshot is not ready. Return to `/kyc/check`.

### 3.7 Sign Form A

Ask UR to sign the rendered Form A text with the user's UR-managed custodial wallet.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/kyc/sign-form`      |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "sessionId": "5f8e7c9a-1111-2222-3333-444455556666",
  "textHash": "0xabc123..."
}
```

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "state": "Register",
    "signature": "0x<65-byte signature>",
    "signerAddress": "0xUSER_UR_EVM_ADDRESS"
  }
}
```

Rules:

* The request does not include a user signature. UR produces the Form A signature using the user's UR-managed wallet.
* `textHash` must match the latest Form A text rendered by UR.
* If user data changes before `/kyc/submit`, call `/kyc/form-a-info` again and re-sign the latest `textHash`.

### 3.8 Submit onboarding

Submit the completed onboarding session for asynchronous account activation.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/kyc/submit`         |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "sessionId": "5f8e7c9a-1111-2222-3333-444455556666"
}
```

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "state": "Submitting",
    "queued": false,
    "awaitingPenny": false,
    "registrationId": ""
  }
}
```

The `registrationId` can be empty in the synchronous response. UR fills downstream activation details asynchronously. Use `fma.account.result` or `/api/fma/v1/account-status` to confirm the final state.

Preconditions:

* `/kyc/check` has returned `complete=true`.
* Form A has been signed through `/kyc/sign-form`.
* The session is in `Register` state.

### 3.9 Get account status

Use account status as a polling fallback after `/kyc/submit`, or as an explicit confirmation before enabling fund-moving features.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `GET`                            |
| Path    | `/api/fma/v1/account-status`     |
| Headers | User-Scoped Partner Auth Headers |

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "status": 5,
    "statusStr": "Live"
  }
}
```

Poll this endpoint at a **1-minute cadence** after `/kyc/submit` if you do not receive `fma.account.result`. Stop polling when `data.statusStr` is `Live`, `Blocked`, or `Closed`.

### 3.10 Onboarding states

Use the following state values for your local onboarding cache:

| State                  | Meaning                                                                       | Partner action                                                                                                                                                                           |
| ---------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PartnerDataIngestion` | UR created the onboarding session and is waiting for a complete KYC snapshot. | Call `/kyc/check` after the Sumsub applicant is complete.                                                                                                                                |
| `IdentityVerification` | UR is still validating identity evidence.                                     | Continue polling `/kyc/check`, or ask the user to complete missing KYC steps.                                                                                                            |
| `SignFormA`            | The KYC snapshot is ready for Form A.                                         | Call `/kyc/form-a-info`, display the text, then call `/kyc/sign-form`.                                                                                                                   |
| `Register`             | Form A is signed and the session is ready to submit.                          | Call `/kyc/submit`.                                                                                                                                                                      |
| `Submitting`           | UR is activating the account with the downstream banking partner.             | Wait for `fma.account.result` or poll `/api/fma/v1/account-status`.                                                                                                                      |
| `Completed`            | The UR Account is activated.                                                  | Enable fund-moving features only after `account-status` returns `Live`.                                                                                                                  |
| `Failed`               | UR rejected or expired the onboarding session.                                | Show the failure state. UR operations may later issue a retry, which arrives as `fma.additional_kyc.required`; see [Retry KYC](https://docs.ur.app/api-reference/kyc-and-kyb/retry-kyc). |

A retry session reports states from this same set, but only the subset its reduced step list covers, so it can start at `SignFormA` with no earlier state ever observed. Treat an unexpected state as "a step remains" rather than an error.

### 3.11 KYC webhooks

UR sends these webhooks to the URL registered for your integration. The first two cover onboarding; the retry events fire later, once the user is already live. Verify every webhook with the same EIP-191 recovery rules in [§13.2](#id-13.2-webhook-signature-verification).

Activation or rejection:

```json
{
  "event": "fma.account.result",
  "timestamp": 1704234567,
  "data": {
    "urId": 7123456789,
    "sessionId": "5f8e7c9a-1111-2222-3333-444455556666",
    "partnerId": "partner",
    "status": "activated",
    "occurredAt": 1704234567
  }
}
```

When `data.status` is `activated`, confirm `Live` with `/api/fma/v1/account-status` before enabling fund-moving features. When `data.status` is `rejected`, the payload includes `rejectCode` and `rejectReason`.

Retry required: UR ops can ask an already-onboarded user to redo part or all of their KYC. That arrives as `fma.additional_kyc.required`:

```json
{
  "event": "fma.additional_kyc.required",
  "timestamp": 1704234567,
  "data": {
    "directiveId": "62fb1d29-e584-448f-a770-9454c94dbe24",
    "type": "retry",
    "taskType": "passport",
    "retryLevel": 6,
    "fiat24Mode": "ops_offline",
    "dataChannel": "sdk",
    "partnerId": "partner_example",
    "externalUserId": "partner-user-0001",
    "urId": 7123456789,
    "retryOfSessionId": "old-session-id",
    "retryReason": "Compliance review: document expired",
    "requiredFields": [],
    "deadlineAt": 0,
    "createdAt": 1704234567
  }
}
```

The new session is not in the payload. Call `POST /api/fma/v1/kyc/session/create` to claim it, then run only the steps `taskType` names. Do not call `/create-account` for a retry. A second event, `fma.additional_kyc.completed`, fires when the retry session finishes. See [Retry KYC](https://docs.ur.app/api-reference/kyc-and-kyb/retry-kyc) for the full walkthrough and [Webhooks](https://docs.ur.app/developer-resources/webhook) for both payload contracts.

***

## 4. Core banking integration principles

The following rules apply across all fund-moving endpoints in this reference.

1. **User balances** are queried with `GET /api/fma/v1/balance` ([§6.1](#id-6.1-get-user-balance)).
2. **Off-ramp** converts crypto (held on a supported source chain) into the user's tokenized fiat balance. For the current set of supported source chains, source tokens, and target fiat currencies, see [Supported Chains & Tokens](https://docs.ur.app/api-reference/account/pages/0KrXzznVQkBgtZweDaXH#id-3.1.9-get-supported-chain-config).
3. **On-ramp** converts the user's tokenized fiat balance into crypto on a supported destination chain. New On-ramp submissions must be blocked while a pending retry exists. For the current set of supported destination chains and tokens, see cryptos with `aggregatorSupported` value in the response of [Supported Chains & Tokens](https://docs.ur.app/api-reference/account/pages/0KrXzznVQkBgtZweDaXH#id-3.1.9-get-supported-chain-config).
4. **Card authorization** behavior depends on the Partner's Card Mode. See [API Reference: Card Mode: Crypto Backed](/api-reference/cards/crypto-backed-card) for the Crypto Backed integration surface; Card Mode: Fiat Only has no Partner-side authorization surface.
5. **All async settlement results** are delivered via the transaction webhook ([§13](#id-13-webhooks)). The webhook is the authoritative source of truth; API responses to fund-moving calls return only a `txHash` (the operation has been submitted on-chain, not yet settled).

***

## 5. Profile

### 5.1 Get BR profile

Fetch the user's banking profile, including IBAN, fiat limits, contacts, deposit bank details, and card eligibility.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `GET`                            |
| Path    | `/api/fma/v1/br`                 |
| Headers | User-Scoped Partner Auth Headers |

Request body: none. Query parameters: none.

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "tokenId": 1000123456,
    "br": "John Doe",
    "iban": "CH9300762011623852957",
    "email": "john@example.com",
    "mobile": "+41xxxx",
    "debitCard": "MSTD",
    "isCardEligible": true,
    "cards": [],
    "cardActivation": {
      "amount": 100,
      "currency": "CHF"
    },
    "street": "Bahnhofstrasse 1",
    "postalCode": "8001",
    "city": "Zurich",
    "country": "CHE",
    "limits": {
      "restartDate": "2026-04-30",
      "restartDateMs": 1777507200000,
      "used": 10000,
      "available": 90000,
      "max": 100000
    },
    "contacts": {
      "EUR": [
        {
          "id": "cnt_001",
          "name": "Acme SA",
          "account": "CH93****2957",
          "fullAccount": "CH9300762011623852957",
          "bank": "UBS",
          "country": "CH",
          "lastPaymentDate": 1713600000000
        }
      ]
    },
    "depositBank": {
      "EUR": {
        "account": "CHxx...",
        "bank": "Bank ABC",
        "BIC": "FNBSCHZZXXX",
        "payee": "UR AG",
        "city": "Zurich",
        "street": "xxxxx",
        "postalCode": "8001",
        "country": "CH"
      }
    }
  }
}
```

Notes:

* `limits` are denominated in **CHF** and use a **rolling 30-day window**.
* FX, card spending, on-ramp, and payout share the same fiat limit bucket; each of these is checked against `limits.available` and fails if it exceeds it.
* A single outgoing transaction must not exceed `limits.available`.
* `iban` is the user's default personal Swiss IBAN. It receives **EUR and CHF** deposits; it does not receive USD.
* `depositBank` is keyed by currency. For each inbound transfer, read the entry whose key matches the deposit currency, and show that account to the user.
* A **USD IBAN is separate** from the EUR/CHF IBAN. UR provisions the EUR/CHF IBAN automatically when the user reaches `Live`, with no prerequisite pay-in. UR provisions the USD IBAN only on request: call `POST /v1/apply-usd-payin` when the user wants to receive USD. The call is synchronous, and UR creates the USD IBAN immediately if the user is `Live`. The USD deposit account then appears under the `USD` key of `depositBank`. Match the account to the currency the user will send; never reuse the EUR/CHF IBAN for a USD transfer. USD deposits from a non-same-name sender are held for review; see [Deposits](https://docs.ur.app/concepts/deposits#usd-deposits-from-a-third-party-account).

***

## 6. Balance

### 6.1 Get user balance

Fetch the user's fiat balances held inside the user's UR account. In Managed Custody Mode the UR-managed account holds tokenized fiat only.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `GET`                            |
| Path    | `/api/fma/v1/balance`            |
| Headers | User-Scoped Partner Auth Headers |

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "fiatItems": [
      { "currency": "EUR", "amount": "1000.50" },
      { "currency": "CHF", "amount": "5000.00" }
    ]
  }
}
```

* `fiatItems` enumerates the user's tokenized fiat balances on Mantle (EUR24, CHF24, USD24, etc.).
* The endpoint returns fiat only. The UR-managed account never custodies crypto, so there is no crypto balance to report; the user's crypto sits in an external (non-UR) wallet.

***

## 7. Off-ramp

> **Off-ramp** converts crypto into fiat in the user's UR account.

**Currently supported:**

* **Source chains and tokens:** see [Supported Chains & Tokens](https://docs.ur.app/api-reference/account/pages/0KrXzznVQkBgtZweDaXH#id-3.1.9-get-supported-chain-config)
* **Target fiat currencies:** USD, EUR, CHF, SGD, JPY, HKD

**Amount limits.** Read the minimum and maximum Off-ramp amount for each source token from the chain config fields `minTopUpAmount` and `maxTopUpAmount`; see [Supported Chains & Tokens](https://docs.ur.app/api-reference/account/pages/0KrXzznVQkBgtZweDaXH#id-3.1.9-get-supported-chain-config). Read them at request time and do not hardcode them, because UR converts a USD anchor at the live rate for each token. A USDC Off-ramp must be at least 5 USDC. Each Off-ramp also counts against the user's rolling 30-day fiat limit.

### Flow

```mermaid
sequenceDiagram
    participant Partner as Partner Frontend
    participant UR as UR OpenAPI
    participant W as External Crypto Wallet<br/>(partner-side or user's own)
    participant SC as UR Off-ramp Contract

    Partner->>UR: 1. POST /quote/deposit (request quote)
    UR-->>Partner: 2. quoteId + best{to, swapCalldata, minUsdcAmount}
    Partner->>W: 3. prompt user to sign
    W->>SC: 4. depositTokenViaAggregatorToAccount(...params, _targetAccount = user's UR Account)
    SC-->>SC: 5. tx receipt
    UR-->>Partner: 6. webhook transaction (data.type = CRYPTO_DEPOSIT)
    Note over SC: Fiat credited to user UR Account (per _targetAccount)
```

The UR API step is quote retrieval. After the Partner receives the quote, the holder of the external crypto wallet (the user, or the Partner when the wallet is partner-side) signs and submits the Off-ramp contract call. See [§7.2](#id-7.2-initiate-off-ramp).

### 7.1 Get off-ramp quote

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/quote/deposit`      |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "chainId": "<source-chain-CAIP2>",
  "fromToken": "0x_SOURCE_TOKEN_ADDRESS",
  "toCurrency": "EUR",
  "amount": "5000"
}
```

* `chainId` & `fromToken`: see [Supported Chains & Tokens](https://docs.ur.app/api-reference/account/pages/0KrXzznVQkBgtZweDaXH#id-3.1.9-get-supported-chain-config)
* `toCurrency`: target fiat currency symbol.
* `amount`: human-readable decimal string; UR converts it to token smallest units using the source token decimals.

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "quoteId": "ur_1772002152589294701",
    "chainId": "<source-chain-CAIP2>",
    "targetAccount": "0x_USER_UR_ACCOUNT_ADDRESS",
    "best": {
      "aggregator": "1inch",
      "to": "0x_AGGREGATOR_CONTRACT_ADDRESS",
      "swapCalldata": "0x12aa...",
      "minUsdcAmount": "4950000",
      "expectedUsdcAmount": "5000000",
      "deadline": 1772002211,
      "priceImpact": "0.05"
    },
    "inputAmount": "5000",
    "outputAmount": "5",
    "exchangeRate": "1",
    "crossChainFee": "111598233453575",
    "networkFee": "3109867200000",
    "amountReceived": "4950000"
  }
}
```

**Tempo chain only:** `amountReceived` is the actual USDC amount received on Arbitrum (in smallest unit). This field is only returned for Tempo chain deposits.

Contract execution notes:

* Pass `best.to`, `best.swapCalldata`, and `best.minUsdcAmount` to the UR Off-ramp contract **exactly as returned**.
* The signing external Crypto Wallet (see [§7.2](#id-7.2-initiate-off-ramp)) must have approved the Off-ramp contract to spend `fromToken` for at least `amount`.
* The transaction must be submitted before `best.deadline`; otherwise it can revert.
* `networkFee` and `crossChainFee` are denominated in the source chain's native token and paid by the user from the source chain wallet (in addition to `amount`).
* Final settlement is reported asynchronously through the transaction webhook with `data.type = "CRYPTO_DEPOSIT"`.

### 7.2 Initiate off-ramp

In **Managed Custody Mode**, the **Fiat Wallet is always UR-managed** and the **Crypto Wallet is always an external (non-UR) wallet**: the partner's account or the user's own. For compliance, the UR-managed account never holds crypto: the source crypto is paid in from that external wallet, and UR credits the resulting fiat to the user's UR Account. The Off-ramp contract uses the `_targetAccount` parameter to identify which UR Account receives the resulting fiat.

**Contract**: `depositTokenViaAggregatorToAccount` on the Off-ramp contract. Contract addresses per chain: see [Deposit (off-ramp)](/api-reference/smart-contracts#deposit-off-ramp).

Contract Parameters:

| Parameter           | Type    | Required | Description                                                                                                 | Example                            |
| ------------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| `_inputToken`       | address | **Yes**  | Source token address (Use `0x00...00` for native tokens).                                                   | `"0xA0b8...B48"` (USDC)            |
| `_outputToken`      | address | **Yes**  | Target fiat token address (Fiat type after deposit).                                                        | `"0x1234...5678"`                  |
| `_amount`           | uint256 | **Yes**  | Deposit amount (in smallest unit, e.g., Wei).                                                               | For USDC: `"10000000"` = 10.000000 |
| `_aggregator`       | address | **Yes**  | Exchange contract address; use `best.to` from §7.1.                                                         |                                    |
| `_swapCalldata`     | bytes   | **Yes**  | Use `best.swapCalldata` from §7.1.                                                                          | `"0x"` for USDC direct deposit.    |
| `_minUsdcAmount`    | uint256 | **Yes**  | Use `best.minUsdcAmount` from §7.1.                                                                         |                                    |
| `_feeAmountViaUsdc` | uint256 | **Yes**  | Put `"0"` when user calls the contract directly.                                                            |                                    |
| `_targetAccount`    | address | **Yes**  | Use `data.targetAccount` from §7.1. This is the user's UR Account address that receives the resulting fiat. |                                    |

**For Tempo chain**, use `depositWithFeeTo` on the Tempo Off-ramp contract. Contract addresses per chain: see [Deposit (off-ramp)](/api-reference/smart-contracts#deposit-off-ramp).

Contract Parameters:

| Parameter           | Type    | Required | Description                                                                                                                                                           |
| ------------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `recipient`         | address | **Yes**  | The user's UR Account address that receives the resulting fiat. Used when the signing wallet is not the user's UR Account itself.                                     |
| `inputToken`        | address | **Yes**  | Source stablecoin address.                                                                                                                                            |
| `inputAmount`       | uint256 | **Yes**  | Deposit amount (in smallest unit, e.g., Wei).                                                                                                                         |
| `outputToken`       | address | **Yes**  | Target fiat token address.                                                                                                                                            |
| `minAmountReceived` | uint256 | **Yes**  | Minimum amount to receive on Arbitrum. Calculate from the quote API: `amountReceived` adjusted by `slippageBps`.                                                      |
| `refundAddress`     | address | **Yes**  | Address to receive refund of excess cross-chain fee. Usually the user's wallet address.                                                                               |
| `maxFeeUsdcAmount`  | uint256 | **Yes**  | First-hop (Tempo → Arbitrum) cross-chain fee budget (denominated in USDC, in smallest unit). Calculate from the quote API: `crossChainFee` adjusted by `slippageBps`. |
| `feeAmountViaUsdc`  | uint256 | **Yes**  | Put "0" when user calls the contract directly.                                                                                                                        |

If you require Partner-side API submission for Off-ramp instead of on-chain user signing, please contact the UR team.

***

## 8. FX and internal transfers

This section covers two on-chain fiat operations. FX converts one tokenized fiat balance into another inside the user's UR Account. Internal transfers send one tokenized fiat balance from the user's UR Account to a different UR Account.

**Amount limits.** Read the minimum and maximum FX amount for each token from the chain config fields `minFxAmount` and `maxFxAmount`; see [Supported Chains & Tokens](https://docs.ur.app/api-reference/account/pages/0KrXzznVQkBgtZweDaXH#id-3.1.9-get-supported-chain-config). Read them at request time and do not hardcode them. UR converts the minimum from a USD anchor at the live rate. The maximum reflects the user's remaining rolling 30-day allowance, so it changes over time.

### 8.1 FX quote

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/quote/fx`           |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "fromCurrency": "EUR",
  "toCurrency": "CHF",
  "inputAmount": "5"
}
```

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "inputAmount": "5",
    "fromCurrency": "EUR",
    "toCurrency": "CHF",
    "outputAmount": "4.18",
    "exchangeRate": "0.83"
  }
}
```

### 8.2 Execute FX

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/fx-exchange`        |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "reqId": "fx-20260423-0001",
  "fromCurrency": "EUR",
  "toCurrency": "CHF",
  "amount": "50",
  "amountOutMinimum": "49.75"
}
```

`amountOutMinimum` is optional. If omitted, UR applies a default 0.5% slippage buffer based on the submitted `amount`.

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "txHash": "0xabc123def456..."
  }
}
```

Final result is reported through the transaction webhook with `data.type = "FRX"`. See [§12.1](#id-12.1-fetch-transaction-history) for the full set of `status` values.

### 8.3 Initiate an internal transfer

Send tokenized fiat from the user's UR Account to another UR Account. The User-Scoped Partner Auth Headers identify the sender. Send the recipient's URID in `toAccountId`; do not put the sender's URID in the request body.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/internal-transfer`  |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "reqId": "transfer-20260804-0001",
  "amount": "25.50",
  "currency": "EUR",
  "toAccountId": "7123456790"
}
```

Request fields:

| Field         | Type   | Required | Description                                                                                     |
| ------------- | ------ | -------- | ----------------------------------------------------------------------------------------------- |
| `reqId`       | string | Yes      | Partner-supplied idempotency key. Keep this value stable for the same logical transfer.         |
| `amount`      | string | Yes      | Positive human-readable decimal amount. Do not send the amount in token smallest units.         |
| `currency`    | string | Yes      | Tokenized fiat currency configured for the endpoint: `EUR`, `CHF`, or `USD`.                    |
| `toAccountId` | string | Yes      | Recipient URID as a non-negative decimal integer string. It must differ from the sender's URID. |

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "txHash": "0xabc123def456..."
  }
}
```

The response confirms submission, not settlement. Confirm the final result through the transaction webhook or query `POST /api/fma/v1/transactions` by `txHash` or `reqId`. The transaction type is `INTERNAL_TOKEN_TRANSFER`; a successful transfer reaches `CONFIRMED`.

UR returns a non-zero business `code` synchronously when the user identity, recipient URID, currency, amount, or `reqId` is invalid. An insufficient balance or an on-chain execution failure can surface asynchronously as `FAILED` or `REJECTED`. Do not credit the recipient based only on the submission response.

***

## 9. Bank payout

> **Bank Payout** sends tokenized fiat from the user's UR account to an external bank account via SEPA / SWIFT.

### Flow

```mermaid
sequenceDiagram
    participant Partner as Partner Backend
    participant UR as UR OpenAPI
    participant Wallet as User UR Account
    participant Bank as Recipient Bank

    Note over Partner, UR: Step 1: Fee List
    Partner->>UR: GET /api/v1/banks/payout/fees
    UR-->>Partner: { EUR: {fee, minimalPayoutAmount, tokenAddress}, CHF: {...} }

    Note over Partner, UR: Step 2: Select Recipient
    alt Recent contact
      Partner->>UR: GET /fma/br (read contacts)
      opt New reference
        Partner->>UR: POST /fma/verify-reference
        UR-->>Partner: refId + purposeId
      end
    else New contact
      Partner->>UR: GET /api/v1/banks (select country)
      alt Country supports IBAN
        Partner->>UR: GET /api/v1/banks/iban/{iban}
      else Non-IBAN country
        Partner->>UR: Select bank from /api/v1/banks + enter account number
      end
      Partner->>UR: GET /api/v1/country-cities (recipient address)
      Partner->>UR: GET /api/v1/payment-purposes (select purpose)
      Partner->>UR: POST /api/fma/v1/verify-contact
      UR-->>Partner: contactId + refId + purposeId
    end

    Note over Partner, UR: Step 3: Execute Payout
    Partner->>UR: POST /api/fma/v1/submit-payout
    UR-->>Wallet: Debit fiat token (UR signs custodial permit)
    UR-->>Partner: txHash

    Note over Partner, Bank: Step 4: Async Settlement
    UR->>Bank: SEPA / SWIFT transfer
    UR-->>Partner: webhook transaction (type=FIAT_WITHDRAW, status=CONFIRMED|REJECTED)
    opt Refund on rejection
      UR-->>Partner: webhook transaction (type=FIAT_WITHDRAW, direction=IN, same refId)
    end
```

### 9.1 Get payout fees

| Item    | Value                       |
| ------- | --------------------------- |
| Method  | `GET`                       |
| Path    | `/api/v1/banks/payout/fees` |
| Headers | Public Metadata Headers     |

Public metadata, not scoped to a single user. Returns the standard envelope:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "EUR": {
      "tokenAddress": "0x0578be9C858e6562dd8cd11a738b89Ca48194dA5",
      "currency": "EUR",
      "fee": "0",
      "minimalPayoutAmount": "1000"
    },
    "CHF": {
      "tokenAddress": "0x53587A05ccDdCE555C2Cd7cE4C9c5Bc3D912E2f3",
      "currency": "CHF",
      "fee": "0",
      "minimalPayoutAmount": "1000"
    }
  }
}
```

### 9.2 Choose recipient

The Partner can use either a recent contact returned by `GET /api/fma/v1/br`, or create / verify a new contact.

**Recent contact path:**

* Read `data.contacts[currency]` from the BR Profile response ([§5.1](#id-5.1-get-br-profile)).
* Use `contact.id` as `contactId`.
* If the user provides a new reference, call `POST /api/fma/v1/verify-reference` ([§9.3](#id-9.3-verify-reference)) to get a fresh `refId` + `purposeId`.

**New contact path:**

* Identify the recipient bank by IBAN (`GET /api/v1/banks/iban/{iban}`) or by selecting from the non-IBAN bank list (`GET /api/v1/banks`).
* Collect required creditor name, address, country, city, payment purpose, and reference.
* Call `POST /api/fma/v1/verify-contact` ([§9.4](#id-9.4-verify-contact)).

Public metadata APIs (`/api/v1/banks`, `/api/v1/banks/iban/{iban}`, `/api/v1/country-cities`, `/api/v1/payment-purposes`) do not require user identity headers unless UR changes their access policy.

### 9.3 Verify reference

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/verify-reference`   |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{ "reference": "Invoice 2026-001" }
```

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "purposeId": 8,
    "refId": "REF-7A6C2A8E"
  }
}
```

### 9.4 Verify contact

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/verify-contact`     |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "account": "CH93 0076 2011 6238 5295 7",
  "bankName": "Hypothekarbank Lenzburg AG",
  "bic": "HYPCH22",
  "purpose": 1,
  "reference": "Invoice 2026-001",
  "creditorInfo": {
    "name": "Alice Doe",
    "street": "Bahnhofstrasse 1",
    "city": "Zurich",
    "zip": "8001",
    "country": "CH"
  }
}
```

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "account": "CH93 0076 2011 6238 5295 7",
    "bankName": "Hypothekarbank Lenzburg AG",
    "bic": "HYPCH22",
    "purpose": 1,
    "reference": "...",
    "clientPayoutRefParams": {
      "contactId": "SP",
      "purposeId": 1,
      "refId": "REF-7A6C2A8E"
    }
  }
}
```

`creditorInfo.name`, `creditorInfo.street`, `creditorInfo.city`, and `creditorInfo.country` must use **Latin characters**.

### 9.5 Submit payout

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/submit-payout`      |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "reqId": "unique-idempotency-key",
  "amount": "250",
  "contactId": "EA-00017418",
  "currency": "EUR",
  "purposeId": "1",
  "refId": "REF-7A6C2A8E",
  "metadata": {
    "bankAccountHolder": "Alice Doe",
    "bankName": "Hypothekarbank Lenzburg AG",
    "bankAccount": "CH9300762011623852957",
    "bankReference": "Invoice 2026-001"
  }
}
```

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "txHash": "0xabc123def456..."
  }
}
```

Constraints:

* `metadata` name, address, and reference values must use **Latin characters**.
* `purposeId` and `refId` must be provided together, or both omitted.
* Minimum amount is currency-specific and comes from `minimalPayoutAmount` ([§9.1](#id-9.1-get-payout-fees)).
* Network fees and payout fees are deducted from `amount`; they are not charged separately.
* Payout is subject to the user's rolling 30-day CHF-denominated fiat limits.
* Final result is reported through the transaction webhook with `data.type = "FIAT_WITHDRAW"`.

***

## 10. On-ramp

{% hint style="warning" %}
**Available soon.** On-ramp (fiat-to-crypto) is not yet available for integration and will be enabled in a future release. The reference below is provided for preview only.
{% endhint %}

> **On-ramp** converts the user's tokenized fiat balance into crypto, delivered to an external wallet on a target chain. The UR-managed account holds fiat only, so On-ramp crypto never lands in the UR account.

**Currently supported:**

* **Destination chains and tokens:** see cryptos with `aggregatorSupported` value in the response of [Supported Chains & Tokens](https://docs.ur.app/api-reference/account/pages/0KrXzznVQkBgtZweDaXH#id-3.1.9-get-supported-chain-config).
* **Source fiat currencies:** USD, EUR, CHF, SGD, JPY, HKD

### Flow

```mermaid
sequenceDiagram
    participant Partner as Partner Backend
    participant UR as UR OpenAPI
    participant LV as Liveness Vendor

    Partner->>UR: 1. GET /api/fma/v1/onramp-limit
    UR-->>Partner: 2. availability + limits
    Partner->>UR: 3. GET /api/fma/v1/onramp/pending-retry
    UR-->>Partner: 4. pending retry data or empty data
    Partner->>UR: 5. POST /api/fma/v1/quote/onramp
    UR-->>Partner: 6. quoteId + needLiveness
    opt needLiveness = true
      Partner->>UR: 7. GET /api/fma/v1/onramp-liveness-token
      Partner->>LV: 8. run liveness check
      Partner->>UR: 9. GET /api/fma/v1/onramp-liveness-result
    end
    Partner->>UR: 10. POST /api/fma/v1/onramp
    UR-->>Partner: 11. txHash
    UR-->>Partner: 12. webhook transaction (data.type = ONRAMP)
```

### 10.1 On-ramp login initialization

When the user enters the On-ramp flow, the Partner should run these checks **in order**:

1. `GET /api/fma/v1/onramp-limit`
2. `GET /api/fma/v1/onramp/pending-retry`, only if the limit response allows the flow.

If a pending retry exists, the Partner **must** force the user to **Retry** or **Cancel** before starting a new On-ramp.

### 10.2 Get on-ramp limit

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `GET`                            |
| Path    | `/api/fma/v1/onramp-limit`       |
| Headers | User-Scoped Partner Auth Headers |

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "livenessLocked": false,
    "livenessLockMins": 0,
    "maxAmounts": {
      "USD": "50000",
      "EUR": "46500"
    },
    "minAmounts": {
      "USD": "5",
      "EUR": "4.65"
    },
    "usdcDepegged": false,
    "regionLocked": false
  }
}
```

Block the flow if `regionLocked`, `usdcDepegged`, or `livenessLocked` is `true`, or if the requested amount falls outside the `minAmounts[currency]` to `maxAmounts[currency]` range.

`maxAmounts` and `minAmounts` are keyed by fiat currency. Read both at request time; do not hardcode them. UR derives them from a single USD anchor (about 5 USD minimum; the maximum depends on the user's liveness state) and converts each to fiat at the live rate, so the values differ across currencies.

### 10.3 Check pending retry

| Item    | Value                              |
| ------- | ---------------------------------- |
| Method  | `GET`                              |
| Path    | `/api/fma/v1/onramp/pending-retry` |
| Headers | User-Scoped Partner Auth Headers   |

Pending retry represents an On-ramp whose **bridge succeeded** but whose **swap leg failed**; the user must resolve it before starting a new On-ramp.

No pending item:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "originalTxHash": "",
    "originalChainId": "",
    "originalCurrency": "",
    "chainId": "",
    "fromToken": "",
    "toToken": "",
    "amount": "",
    "amountRaw": "",
    "failedAt": 0
  }
}
```

When there is no pending retry, `data.originalTxHash` is empty.

Pending item:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "originalTxHash": "0xoriginal...",
    "originalChainId": "<src-chain-CAIP2>",
    "originalCurrency": "EUR",
    "chainId": "<dst-chain-CAIP2>",
    "fromToken": "0x_BRIDGE_INTERMEDIATE_TOKEN",
    "toToken": "0x_DESTINATION_TOKEN_ADDRESS",
    "amount": "9.98",
    "amountRaw": "9980000",
    "failedAt": 1703123000
  }
}
```

### 10.4 Get on-ramp quote

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/quote/onramp`       |
| Headers | User-Scoped Partner Auth Headers |

Request body for the main On-ramp flow:

```json
{
  "scene": "onramp",
  "srcChainId": "<src-chain-CAIP2>",
  "dstChainId": "<dst-chain-CAIP2>",
  "fromCurrency": "EUR",
  "toToken": "0x_DESTINATION_TOKEN_ADDRESS",
  "amount": "100.50",
  "slippageBps": 50
}
```

Notes:

* `scene` is `onramp` for the main flow and `swap_retry` for retry ([§10.8](#id-10.8-retry-on-ramp-swap)).
* `srcChainId` is the chain where the user's tokenized fiat is held (UR's home chain).
* `fromCurrency` is required for `scene = "onramp"`. `fromToken` is used only for `scene = "swap_retry"`.
* `dstChainId` and `toToken` must match a crypto type with `aggregatorSupported` value in the response of [Supported Chains & Tokens](https://docs.ur.app/api-reference/account/pages/0KrXzznVQkBgtZweDaXH#id-3.1.9-get-supported-chain-config).
* `networkFee` returned in the quote response is the destination-chain gas + cross-chain fee, deducted from the user's input fiat.
* If the response has `needLiveness = true`, the Partner **must** complete liveness before submitting On-ramp.

### 10.5 Liveness token

| Item    | Value                               |
| ------- | ----------------------------------- |
| Method  | `GET`                               |
| Path    | `/api/fma/v1/onramp-liveness-token` |
| Headers | User-Scoped Partner Auth Headers    |

Only call this endpoint when a quote returns `needLiveness = true`.

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "vendor": "sumsub",
    "access_token": "sumsub_access_token_xxx",
    "user_id": "sumsub_user_id_xxx"
  }
}
```

### 10.6 Liveness result

| Item    | Value                                |
| ------- | ------------------------------------ |
| Method  | `GET`                                |
| Path    | `/api/fma/v1/onramp-liveness-result` |
| Headers | User-Scoped Partner Auth Headers     |

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "liveness_result": "pass",
    "checked_at": 1703123000,
    "expired_at": 1703727800,
    "liveness_fail_reason": "",
    "liveness_locked": false,
    "liveness_unlock_at": 0
  }
}
```

After `liveness_result = "pass"`, the Partner should request a **new quote** and submit using the new `quoteId`.

### 10.7 Submit on-ramp

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/onramp`             |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "reqId": "onramp-2026-04-24-0001",
  "quoteId": "onramp_direct_1703123000000_12345",
  "fromCurrency": "EUR",
  "chainId": "<src-chain-CAIP2>",
  "amountIn": "100",
  "dstChainId": "<dst-chain-CAIP2>",
  "withdrawAddress": "0x_EXTERNAL_WALLET_ADDRESS",
  "dstAggregator": "0xAggregatorAddress",
  "dstTokenOut": "0x_DESTINATION_TOKEN_ADDRESS",
  "dstSwapCalldata": "0x...",
  "dstMinAmountOut": "1228327"
}
```

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": { "txHash": "0xabc123..." }
}
```

Submit constraints:

* `amountIn` is a human-readable decimal string and must match the amount used for the cached quote.
* `quoteId` must match UR's cached quote (and not be expired).
* A quote requiring liveness cannot be submitted until liveness passes.
* New On-ramp must be **blocked** while a pending retry exists ([§10.3](#id-10.3-check-pending-retry)).
* `withdrawAddress` is the external wallet that receives the crypto. It is **required for every On-ramp**, same-chain or cross-chain, because the UR-managed account holds fiat only and never receives crypto.
* UR delivers the destination-chain crypto to `withdrawAddress`; UR never sends On-ramp crypto to the user's UR account.
* Final result is reported through the transaction webhook.

### 10.8 Retry on-ramp swap

On-ramp is a two-leg flow (bridge + swap). When the bridge succeeds but the swap fails, the user's funds are stuck as the bridge intermediate token on the destination chain. Retry redoes the swap leg only, so the payload drops fiat/source-chain inputs and instead carries `originalTxHash`, the post-bridge intermediate-token amount, and a fresh swap quote.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/onramp-swap`        |
| Headers | User-Scoped Partner Auth Headers |

Only call this endpoint when `GET /api/fma/v1/onramp/pending-retry` returns a pending item.

Retry flow:

1. Read pending retry from `GET /api/fma/v1/onramp/pending-retry`.
2. Request a fresh retry quote:
   * `scene = "swap_retry"`
   * `srcChainId = pendingRetry.chainId`
   * `dstChainId = pendingRetry.chainId`
   * `fromToken = pendingRetry.fromToken`
   * `toToken = pendingRetry.toToken`
   * `amount = pendingRetry.amount` (human-readable)
3. Submit `/api/fma/v1/onramp-swap`:
   * `usdcAmount = pendingRetry.amountRaw` (USDC minimal unit)
   * `tokenOut = pendingRetry.toToken`
   * `minAmountOut = quote.best.minAmountOut`
   * `aggregator = quote.best.to` (not `quote.best.aggregator`)
   * `swapCalldata = quote.best.swapCalldata`

Request body:

```json
{
  "reqId": "onramp-retry-2026-04-24-0001",
  "quoteId": "1inch_1703123000000_12345",
  "chainId": "<dst-chain-CAIP2>",
  "originalTxHash": "0xoriginal...",
  "usdcAmount": "9980000",
  "tokenOut": "0x_DESTINATION_TOKEN_ADDRESS",
  "minAmountOut": "1228327",
  "aggregator": "0xAggregatorAddress",
  "swapCalldata": "0x..."
}
```

### 10.9 Cancel on-ramp retry

| Item    | Value                             |
| ------- | --------------------------------- |
| Method  | `POST`                            |
| Path    | `/api/fma/v1/onramp/retry/cancel` |
| Headers | User-Scoped Partner Auth Headers  |

Request body:

```json
{ "originalTxHash": "0xoriginal..." }
```

A successful response clears the pending retry record and allows the Partner to re-enable the normal On-ramp entry point.

***

## 11. Card

> The user's debit card is issued and processed by UR through Mastercard. This section covers only the card-management endpoints common to all Card Modes: card creation, card info retrieval, default-currency selection, and post-settlement history.
>
> **Card authorization, prefund, and card-related webhooks are Card-Mode-specific.** Card Mode: Fiat Only has no Partner-side authorization surface; UR handles authorization on-chain against the user's tokenized fiat balance. Card Mode: Crypto Backed has its own integration surface (synchronous authorization callback, Prefund Account, Prefund Balance Alert webhook) documented in [**API Reference: Card Mode: Crypto Backed**](/api-reference/cards/crypto-backed-card).

### 11.1 Create card

Create a virtual card for an eligible Live user.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/open-card`          |
| Headers | User-Scoped Partner Auth Headers |

Request body: `{}`

Preconditions:

* `GET /api/fma/v1/br` returns `isCardEligible = true`.
* The user has no existing card if UR only allows one card per user.
* The user balance satisfies `cardActivation.amount` and `cardActivation.currency`.

Response example:

```json
{ "code": 0, "message": "" }
```

### 11.2 Get card info

Fetch card metadata and a short-lived `cardToken` for secure card display.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `GET`                            |
| Path    | `/api/fma/v1/card`               |
| Headers | User-Scoped Partner Auth Headers |

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "security": {
      "contactlessEnabled": true,
      "withdrawalEnabled": false,
      "internetPurchaseEnabled": true,
      "overallLimitsEnabled": true
    },
    "currencies": ["EUR", "CHF", "USD"],
    "tokenId": 106654866313,
    "limits": {
      "account": {
        "restartDate": "01.02.2026 9:47",
        "restartDateMs": 1769939264000,
        "used": 33645.39,
        "available": 760005.39,
        "max": 793650.79
      },
      "withdrawal": { "used": 0, "max": 0 },
      "internetPurchase": { "used": 4858.63, "max": 165010 }
    },
    "cardDesign": "MSTDMNT",
    "cardHolder": "John Doe",
    "status": "Active",
    "currency": "EUR",
    "masked": {
      "cardNumber": ".... 3083",
      "cvv2": "...",
      "expiry": "../.."
    },
    "cardToken": "************************************************",
    "activeTokens": [
      {
        "id": "704ab18a...",
        "type": "iPhone 16 pro (Apple Pay)",
        "createdAt": "2026-01-06T16:40:26Z"
      }
    ],
    "externalId": "1758893252"
  }
}
```

Display card details:

The card info API does not expose real PAN, CVV, or expiry in JSON. Use `cardToken` only to render those sensitive fields through UR's card display script. The `cardToken` is short-lived and expires after 5 minutes. When it expires, call `GET /api/fma/v1/card` again to get a fresh token.

Card identifiers:

| Field               | Use                                                                                                    |
| ------------------- | ------------------------------------------------------------------------------------------------------ |
| `cardToken`         | Short-lived token for card detail display only. Do not store or log it.                                |
| `externalId`        | Stable card management ID. Use it for APIs such as Set Default Card Currency.                          |
| `activeTokens[].id` | Device wallet token ID, such as Apple Pay. Do not use it for card detail display or currency settings. |

Load the script from UR:

```html
<script src="https://openapi.ur.app/api/v1/card-display/card.js"></script>
```

Add DOM placeholders where the script should render sensitive fields:

```html
<div class="card-details">
  <div class="card-number-row">
    <div id="cardNumbers"></div>
    <button type="button" id="cardNumbersCopy" aria-label="Copy card number"></button>
  </div>
  <div class="card-meta-row">
    <span id="cardExpiryDate"></span>
    <span id="cardCvvDate"></span>
  </div>
</div>
```

Initialize the display after the user chooses to reveal card details:

```js
const mobile = window.matchMedia("(max-width: 640px)").matches;
const cardTextStyle = {
  background: "transparent",
  color: "#000",
  "font-size": mobile ? "1em" : "23px",
  "font-family": "\"Helvetica Neue\", Helvetica, Arial, sans-serif",
  "letter-spacing": "2px",
  "font-weight": "500"
};

window.fiat24card.bootstrap({
  clientAccessToken: cardToken,
  component: {
    showPan: {
      cardPan: {
        domId: "cardNumbers",
        format: true,
        styles: { span: cardTextStyle }
      },
      copyCardPan: {
        domId: "cardNumbersCopy",
        mode: "transparent",
        onCopySuccess: () => console.log("Card number copied"),
        onCopyFailure: error => console.error("Unable to copy card number", error)
      },
      cardExp: {
        domId: "cardExpiryDate",
        format: true,
        styles: { span: cardTextStyle }
      },
      cardCvv: {
        domId: "cardCvvDate",
        styles: { span: cardTextStyle }
      }
    }
  },
  callbackEvents: {
    onSuccess: () => console.log("Card details rendered"),
    onFailure: error => console.error("Unable to render card details", error)
  }
});
```

Common mistakes:

* Do not store or log `cardToken`.
* Do not use `activeTokens[].id` unless calling a device-token management API.
* Load the script only on the card details view or secure webview, not globally across your app.
* Render card details only after explicit user action, such as selecting "Show card details".

### 11.3 Set default card currency

Set the user's default card transaction currency.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/card-currency`      |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "currency": "USD"
}
```

Request fields:

| Field      | Type   | Required | Description                                                          |
| ---------- | ------ | -------- | -------------------------------------------------------------------- |
| `currency` | string | Yes      | Target default transaction currency, such as `USD`, `EUR`, or `CHF`. |

Response example:

```json
{ "code": 0, "message": "" }
```

Notes:

* The server automatically resolves the card's `externalId` from the authenticated user; the Partner does not need to pass it.
* The next `GET /api/fma/v1/card` response should show the updated `currency`.
* This setting affects UR's default refund currency display and debit preference.
* Per-transaction overrides at swipe time are governed by the Card Mode; see [API Reference: Card Mode: Crypto Backed](/api-reference/cards/crypto-backed-card#id-4-card-authorization-callback-ur-greater-than-partner).

### 11.4 Update card status

Block or unblock the user's card.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/update-card-status` |
| Headers | User-Scoped Partner Auth Headers |

Request body:

```json
{
  "statusChange": "block"
}
```

Request fields:

| Field          | Type   | Required | Description                                                          |
| -------------- | ------ | -------- | -------------------------------------------------------------------- |
| `statusChange` | string | Yes      | The status transition to apply. Accepted values: `block`, `unblock`. |

Response example:

```json
{ "code": 0, "message": "" }
```

Notes:

* The server automatically resolves the card's `externalId` from the authenticated user.
* A blocked card will decline all authorization attempts until unblocked.

### 11.5 Card authorization

Card authorization behavior is **Card-Mode-specific** and is not covered in this document. See [API Reference: Card Mode: Crypto Backed](/api-reference/cards/crypto-backed-card#id-4-card-authorization-callback-ur-greater-than-partner) for the Crypto Backed integration surface. Card Mode: Fiat Only has no Partner-side authorization surface.

### 11.6 Card settlement notes

Card settlement result records are exposed through transaction history ([§12](#id-12-transactions)) with `data.type = "CRD"`. Additional card adjustment event contracts (chargebacks, fee adjustments) must be agreed separately with UR.

***

## 12. Transactions

Use transaction history for reconciliation. The same endpoint also supports exact lookup by transaction hash or transaction ID.

### 12.1 Fetch transaction history

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/transactions`       |
| Headers | User-Scoped Partner Auth Headers |

Simple first-page request:

```json
{
  "pageSize": 20
}
```

Filtered request:

```json
{
  "pageSize": 20,
  "txTypes": [
    "CRYPTO_DEPOSIT",
    "INTERNAL_TOKEN_TRANSFER",
    "UNKNOWN",
    "FX_EXCHANGE",
    "MARQETA_AUTHORIZE",
    "FIAT_WITHDRAW",
    "FIAT_DEPOSIT",
    "ONRAMP"
  ],
  "currencies": ["eur"],
  "direction": "OUT"
}
```

Response example:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "items": [
      {
        "id": 150650,
        "txHash": "0x3051...",
        "txLogIndex": 0,
        "blockNumber": 12345678,
        "createTimeE9": 1779275635000000000,
        "broadcastTimeE9": 1779275636000000000,
        "finalTimeE9": 1779275640000000000,
        "type": "MARQETA_AUTHORIZE",
        "chainId": "eip155:5000",
        "chainName": "Mantle",
        "urId": "7123456789",
        "direction": "OUT",
        "amount": "-36.90",
        "currency": "usd",
        "status": "PENDING",
        "reqId": "fx-20260423-0001",
        "detailsJson": "{\"merchant\":\"Merchant ABC\"}",
        "refundType": ""
      }
    ],
    "hasNextPage": true,
    "hasPrevPage": false,
    "nextCursor": {
      "timestamp": 1779275635000000000,
      "id": 150650
    },
    "prevCursor": {
      "timestamp": 0,
      "id": 0
    },
    "currentPageSize": 1
  }
}
```

Pagination is driven by the response flags and cursors. Only send cursor fields returned by the API, and only when the corresponding flag is `true`.

To fetch the next page, use `nextCursor` when `hasNextPage` is `true`:

```json
{
  "pageSize": 20,
  "cursorTimestamp": 1779275635000000000,
  "cursorId": 150650
}
```

To fetch the previous page, use `prevCursor` when `hasPrevPage` is `true`:

```json
{
  "pageSize": 20,
  "prevCursorTimestamp": 1779275635000000000,
  "prevCursorId": 150650
}
```

If `hasPrevPage` is `false`, do not use `prevCursor`; a zero cursor means there is no previous page.

Request fields:

| Field                 | Type      | Description                                                                                                                     |
| --------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `pageSize`            | integer   | Page size. Defaults to `50`; maximum is `100`.                                                                                  |
| `type`                | string    | Single transaction type filter. Do not send together with `txTypes`.                                                            |
| `txTypes`             | string\[] | Multiple transaction type filter. Do not send together with `type`.                                                             |
| `currencies`          | string\[] | Currency filter. Values are normalized to lowercase by UR.                                                                      |
| `direction`           | string    | Direction filter: `IN`, `OUT`, or `ALL`.                                                                                        |
| `status`              | string    | Transaction status filter.                                                                                                      |
| `chainId`             | string    | Chain ID filter, for example `eip155:5000`.                                                                                     |
| `tokenSymbol`         | string    | Token symbol filter, for example `USDC`.                                                                                        |
| `minAmount`           | string    | Minimum amount filter.                                                                                                          |
| `maxAmount`           | string    | Maximum amount filter.                                                                                                          |
| `fromTimestamp`       | integer   | Start timestamp filter.                                                                                                         |
| `toTimestamp`         | integer   | End timestamp filter.                                                                                                           |
| `cursorTimestamp`     | integer   | Next-page cursor timestamp from `data.nextCursor.timestamp`.                                                                    |
| `cursorId`            | integer   | Next-page cursor ID from `data.nextCursor.id`.                                                                                  |
| `prevCursorTimestamp` | integer   | Previous-page cursor timestamp from `data.prevCursor.timestamp`.                                                                |
| `prevCursorId`        | integer   | Previous-page cursor ID from `data.prevCursor.id`.                                                                              |
| `id`                  | integer   | Exact lookup by UR internal transaction ID. Mutually exclusive with `txHash` and `reqId`.                                       |
| `txHash`              | string    | Exact lookup by transaction hash. Mutually exclusive with `id` and `reqId`.                                                     |
| `reqId`               | string    | Exact lookup by the idempotency key (`reqId`) used when the transaction was created. Mutually exclusive with `id` and `txHash`. |

Partner-relevant transaction types:

| `type`                    | Meaning                                      |
| ------------------------- | -------------------------------------------- |
| `CRYPTO_DEPOSIT`          | Crypto deposit / tokenized fiat top-up.      |
| `INTERNAL_TOKEN_TRANSFER` | Internal token transfer between UR accounts. |
| `UNKNOWN`                 | Unknown or uncategorized transaction type.   |
| `FX_EXCHANGE`             | FX exchange.                                 |
| `MARQETA_AUTHORIZE`       | Card authorization / card payment.           |
| `FIAT_WITHDRAW`           | Fiat withdrawal / bank payout.               |
| `FIAT_DEPOSIT`            | Fiat deposit / bank pay-in.                  |
| `ONRAMP`                  | On-ramp.                                     |

Transaction status values:

| `status`       | Description                                                                                  |
| -------------- | -------------------------------------------------------------------------------------------- |
| `UNKNOWN`      | Unknown; default value. Should not appear in normal flows.                                   |
| `INIT`         | Transaction created internally but not yet broadcast to the blockchain node.                 |
| `PENDING`      | Transaction is in the mempool (transaction pool), awaiting confirmation.                     |
| `CONFIRMED`    | Transaction has been confirmed and settled on-chain.                                         |
| `FAILED`       | Transaction execution failed on-chain.                                                       |
| `PENDING_DROP` | Transaction is scheduled to be dropped, for example replaced or cancelled.                   |
| `DROPPED`      | Transaction was removed from the mempool without being confirmed.                            |
| `REJECTED`     | Transaction was rejected by UR's validation or compliance checks before or during execution. |

### 12.2 Fetch transaction details

Use this endpoint to fetch a single transaction. Send exactly one lookup key: `txHash` for an on-chain transaction hash, `id` for UR's internal transaction ID, or `reqId` for the idempotency key used when the transaction was created.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/transactions`       |
| Headers | User-Scoped Partner Auth Headers |

Lookup by transaction hash:

```json
{
  "txHash": "0x1234567890abcdef..."
}
```

Lookup by UR internal transaction ID:

```json
{
  "id": 99887766
}
```

Lookup by idempotency key:

```json
{
  "reqId": "fx-20260423-0001"
}
```

Response:

* The response uses the same `data.items[]` transaction structure as [§12.1](#id-12.1-fetch-transaction-history).
* Send exactly one of `id`, `txHash`, or `reqId`; they are mutually exclusive.
* If no transaction matches the lookup key, `data.items[]` is empty.

### 12.3 `detailsJson` reference by transaction type

The `detailsJson` field is a **stringified JSON** blob whose structure depends on the transaction `type`. Parse the string before reading nested fields.

> Field names follow protobuf camelCase conventions. All fields are optional (omitempty) unless stated otherwise.

***

#### `CRYPTO_DEPOSIT` (crypto top-up)

Crypto deposit to fund a fiat account.

```json
{
  "fromChainId": "eip155:1",
  "fromTxHash": "0xabc…",
  "fromTxLogIndex": "0",
  "inputToken": "USDC",
  "inputAmount": "100.00",
  "inputTokenAddress": "0x…",
  "bankTxHash": "0xdef…",
  "externalSender": "0x…",
  "externalTarget": "0x…",
  "externalTargetTokenId": "1000001234",
  "partnerId": "partner-001",
  "partnerRefId": "ref-001"
}
```

| Field                   | Type   | Description                                             |
| ----------------------- | ------ | ------------------------------------------------------- |
| `fromChainId`           | string | Source chain ID (e.g., `eip155:1` for Ethereum mainnet) |
| `fromTxHash`            | string | Transaction hash on the source chain                    |
| `fromTxLogIndex`        | string | Log index on the source chain                           |
| `inputToken`            | string | Input token symbol (e.g., `USDC`, `USDT`)               |
| `inputAmount`           | string | Input token amount                                      |
| `inputTokenAddress`     | string | Input token contract address                            |
| `bankTxHash`            | string | Settlement transaction hash on Mantle chain             |
| `externalSender`        | string | External sender address                                 |
| `externalTarget`        | string | External target address                                 |
| `externalTargetTokenId` | string | Target token ID on UR                                   |
| `partnerId`             | string | Partner identifier                                      |
| `partnerRefId`          | string | Partner reference ID                                    |

***

#### `FX_EXCHANGE` (forex exchange)

Fiat-to-fiat currency exchange.

```json
{
  "inputAmount": "100.00",
  "outputAmount": "108.50",
  "inputCurrency": "USD",
  "outputCurrency": "EUR",
  "partnerId": "partner-001",
  "partnerRefId": "fx-20260423-0001"
}
```

| Field            | Type   | Description                        |
| ---------------- | ------ | ---------------------------------- |
| `inputAmount`    | string | Amount of input (sold) currency    |
| `outputAmount`   | string | Amount of output (bought) currency |
| `inputCurrency`  | string | Input currency code (e.g., `USD`)  |
| `outputCurrency` | string | Output currency code (e.g., `EUR`) |
| `partnerId`      | string | Partner identifier                 |
| `partnerRefId`   | string | Partner reference ID               |

***

#### `INTERNAL_TOKEN_TRANSFER` (P2P transfer)

Peer-to-peer transfer between UR accounts.

```json
{
  "fromAddress": "0x…",
  "toAddress": "0x…",
  "fromURId": "1000001234",
  "toURId": "1000005678",
  "fromNickName": "Alice",
  "toNickName": "Bob"
}
```

| Field          | Type   | Description              |
| -------------- | ------ | ------------------------ |
| `fromAddress`  | string | Sender wallet address    |
| `toAddress`    | string | Recipient wallet address |
| `fromURId`     | string | Sender UR ID             |
| `toURId`       | string | Recipient UR ID          |
| `fromNickName` | string | Sender display name      |
| `toNickName`   | string | Recipient display name   |

> **Special `fromURId` values:** `9102` / `9101` / `9103` indicate a bank transfer refund. `9110` / `9113` indicate a card spending refund. `982` indicates UR Rewards.

***

#### `MARQETA_AUTHORIZE` (card payment)

Card authorization / card spending.

```json
{
  "authorizationId": "card-auth-0001",
  "originAuthorizationId": "",
  "merchantId": "merchant-0001",
  "merchant": "Merchant ABC",
  "mcc": 5411,
  "city": "Singapore",
  "country": "SGP",
  "cardId": "card-id-0001",
  "cardCurrency": "USD",
  "transactionCurrency": "USD",
  "settlementCurrency": "EUR",
  "transactionAmount": "36.90",
  "settlementAmount": "33.50",
  "originalPaidCurrency": "USD",
  "subEvent": "",
  "multiTokenList": [],
  "reason": ""
}
```

| Field                           | Type   | Description                                                    |
| ------------------------------- | ------ | -------------------------------------------------------------- |
| `authorizationId`               | string | Card authorization identifier                                  |
| `originAuthorizationId`         | string | Original authorization ID (for refunds/adjustments)            |
| `merchantId`                    | string | Merchant identifier                                            |
| `merchant`                      | string | Merchant name                                                  |
| `mcc`                           | uint64 | Merchant Category Code                                         |
| `city`                          | string | Transaction city                                               |
| `country`                       | string | Transaction country (ISO 3166 alpha-3)                         |
| `cardId`                        | string | Card identifier                                                |
| `cardCurrency`                  | string | Cardholder's default currency                                  |
| `transactionCurrency`           | string | Currency required by the merchant                              |
| `settlementCurrency`            | string | Settlement currency (default `CHF`)                            |
| `transactionAmount`             | string | Amount in transaction currency                                 |
| `settlementAmount`              | string | Amount in settlement currency                                  |
| `originalPaidCurrency`          | string | Original paid currency                                         |
| `subEvent`                      | string | Sub-event type (for crypto-backed card spending)               |
| `multiTokenList`                | array  | Multi-token deductions (crypto-backed mode only)               |
| `multiTokenList[].token`        | string | Token symbol deducted (e.g., `USDe`)                           |
| `multiTokenList[].value`        | string | Equivalent fiat value                                          |
| `multiTokenList[].amount`       | string | Token amount deducted                                          |
| `multiTokenList[].tokenAddress` | string | Token contract address                                         |
| `reason`                        | string | Failure/rejection reason (present only on failed transactions) |

***

#### `FIAT_DEPOSIT` (cash deposit / bank pay-in)

Fiat deposit via bank transfer.

```json
{
  "accountHolder": "Alice Smith",
  "bankName": "DBS Bank",
  "ref": "PAY-20260423-001",
  "account": "SG1234567890",
  "fee": "0.50",
  "contactId": "contact-001"
}
```

| Field           | Type   | Description                   |
| --------------- | ------ | ----------------------------- |
| `accountHolder` | string | Depositor account holder name |
| `bankName`      | string | Bank name                     |
| `ref`           | string | Bank reference number         |
| `account`       | string | Bank account number / IBAN    |
| `fee`           | string | Fee amount (if applicable)    |
| `contactId`     | string | Contact identifier            |

***

#### `FIAT_WITHDRAW` (cash payout / bank withdrawal)

Fiat withdrawal via bank transfer.

```json
{
  "txId": "tx-001",
  "bankRef": "REF-20260423-001",
  "contactId": "contact-001",
  "accountHolder": "Alice Smith",
  "bankName": "DBS Bank",
  "account": "SG1234567890",
  "fee": "1.00",
  "purposeId": "PUR-001"
}
```

| Field           | Type   | Description                |
| --------------- | ------ | -------------------------- |
| `txId`          | string | Internal transaction ID    |
| `bankRef`       | string | Bank reference number      |
| `contactId`     | string | Contact identifier         |
| `accountHolder` | string | Account holder name        |
| `bankName`      | string | Bank name                  |
| `account`       | string | Bank account number / IBAN |
| `fee`           | string | Fee amount (if applicable) |
| `purposeId`     | string | Purpose of payment ID      |

The details may also include a `bankMetadata` object with settlement tracking:

| Field                             | Type   | Description                             |
| --------------------------------- | ------ | --------------------------------------- |
| `bankMetadata.initiate.hash`      | string | Initiation transaction hash             |
| `bankMetadata.initiate.timestamp` | int64  | Initiation timestamp                    |
| `bankMetadata.initiate.txHashUrl` | string | Block explorer URL                      |
| `bankMetadata.payout.hash`        | string | Payout transaction hash                 |
| `bankMetadata.payout.timestamp`   | int64  | Payout timestamp                        |
| `bankMetadata.payout.txHashUrl`   | string | Block explorer URL                      |
| `bankMetadata.return.hash`        | string | Return transaction hash (if returned)   |
| `bankMetadata.return.timestamp`   | int64  | Return timestamp                        |
| `bankMetadata.return.txHashUrl`   | string | Block explorer URL                      |
| `bankMetadata.bic`                | string | BIC / SWIFT code                        |
| `bankMetadata.UETR`               | string | Unique End-to-end Transaction Reference |
| `bankMetadata.creditor.country`   | string | Creditor country                        |
| `bankMetadata.debitor.country`    | string | Debitor country                         |

***

#### `ONRAMP` (fiat to crypto)

Convert fiat token to crypto asset. May involve cross-chain bridging.

```json
{
  "inputChainId": "eip155:5000",
  "inputToken": "0x…",
  "inputAmount": "100.00",
  "outputChainId": "eip155:1",
  "outputToken": "0x…",
  "expectOutputAmount": "99.50",
  "actualOutputAmount": "99.48",
  "expectUsdcAmount": "100.00",
  "actualUsdcAmount": "99.98",
  "guid": "0x…",
  "destination": {
    "status": "CONFIRMED",
    "txHash": "0x…",
    "txTime": 1735689600
  },
  "retryHistory": [],
  "retryStatus": "no_retry",
  "balance": "1000.00",
  "partnerId": "partner-001",
  "partnerRefId": "onr-20260423-0001"
}
```

| Field                | Type   | Description                                                                          |
| -------------------- | ------ | ------------------------------------------------------------------------------------ |
| `inputChainId`       | string | Source chain ID (where fiat token lives)                                             |
| `inputToken`         | string | Input token address (e.g., USD24)                                                    |
| `inputAmount`        | string | Fiat input amount                                                                    |
| `outputChainId`      | string | Destination chain ID                                                                 |
| `outputToken`        | string | Output token address (e.g., USDC, ETH)                                               |
| `expectOutputAmount` | string | Expected output amount (quote)                                                       |
| `actualOutputAmount` | string | Actual output amount (after execution)                                               |
| `expectUsdcAmount`   | string | Expected intermediate USDC amount                                                    |
| `actualUsdcAmount`   | string | Actual intermediate USDC amount                                                      |
| `guid`               | string | Cross-chain unique identifier (LayerZero GUID)                                       |
| `destination`        | object | Destination chain transaction info                                                   |
| `destination.status` | string | Destination chain transaction status                                                 |
| `destination.txHash` | string | Destination chain transaction hash                                                   |
| `destination.txTime` | int64  | Destination chain transaction time                                                   |
| `retryHistory`       | array  | Array of prior destination attempts (same shape as `destination`)                    |
| `retryStatus`        | string | `can_retry` / `retrying` / `retry_success` / `retry_fail` / `no_retry` / `cancelled` |
| `balance`            | string | Balance after transaction                                                            |
| `partnerId`          | string | Partner identifier                                                                   |
| `partnerRefId`       | string | Partner reference ID                                                                 |

***

#### Other types

| Type      | `detailsJson`                                                  |
| --------- | -------------------------------------------------------------- |
| `UNKNOWN` | No structured details. May contain arbitrary JSON or be empty. |

***

### 12.4 Identifying refund transactions

Every transaction record carries a `refundType` field that tells you whether the transaction is a refund and what kind:

| `refundType`        | Meaning                                                                                                                             |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `""` (empty string) | Not a refund (default).                                                                                                             |
| `BANK_REFUND`       | Bank transfer refund. UR returned funds from a system bank account (`fromURId` in `9101/9102/9103`).                                |
| `CARD_REFUND`       | Card spending refund. UR returned card spending funds to the user via a P2P transfer from account `9113`.                           |
| `CARD_REVERSAL`     | Card authorization reversal. The card network reversed a prior authorization, or UR reversed from the card-booked account (`9110`). |

Use `refundType` as the primary discriminator.

#### Rule 1: bank transfer refund (`BANK_REFUND`)

A P2P transfer where `fromURId` is one of the UR system bank accounts:

| `fromURId` | Account role                        |
| ---------- | ----------------------------------- |
| `9101`     | Bank deposit (mint) account         |
| `9102`     | Bank payout account                 |
| `9103`     | Sundry / failed transaction account |

#### Rule 2: card spending refund (`CARD_REFUND`)

A P2P transfer where UR's card settlement system returned funds to the user:

| `fromURId` | Account role                 |
| ---------- | ---------------------------- |
| `9113`     | Card spending refund account |

#### Rule 3: card authorization reversal (`CARD_REVERSAL`)

Appears in two forms:

* **Card network reversal**: `type = MARQETA_AUTHORIZE`, `direction = IN`. The `originAuthorizationId` field in `detailsJson` references the original card authorization.
* **Card-booked account reversal**: `type = INTERNAL_TOKEN_TRANSFER`, `direction = IN`, `fromURId = 9110` (on-chain `CARD_BOOKED` constant).

#### Summary

| Refund scenario              | `refundType`    | `type`                    | `fromURId`               |
| ---------------------------- | --------------- | ------------------------- | ------------------------ |
| Bank transfer refund         | `BANK_REFUND`   | `INTERNAL_TOKEN_TRANSFER` | `9101` / `9102` / `9103` |
| Card spending refund         | `CARD_REFUND`   | `INTERNAL_TOKEN_TRANSFER` | `9113`                   |
| Card reversal (card network) | `CARD_REVERSAL` | `MARQETA_AUTHORIZE`       | N/A                      |
| Card reversal (on-chain)     | `CARD_REVERSAL` | `INTERNAL_TOKEN_TRANSFER` | `9110`                   |

***

### 12.5 Partner-level transaction history

Query all transactions across all users under your partner account. This is a partner-scoped endpoint and does not require `X-Ur-Id` or `X-External-User-Id` headers.

| Item    | Value                                            |
| ------- | ------------------------------------------------ |
| Method  | `POST`                                           |
| Path    | `/api/fma/v1/partner-transactions`               |
| Headers | Partner Auth Headers (no user identity required) |

Request body:

```json
{
  "pageSize": 20,
  "txTypes": ["CRYPTO_DEPOSIT", "FIAT_WITHDRAW"],
  "status": "CONFIRMED"
}
```

The request accepts the same fields as [§12.1 Fetch transaction history](#id-12.1-fetch-transaction-history): `pageSize`, `type`, `txTypes`, `currencies`, `direction`, `status`, `chainId`, `tokenSymbol`, `minAmount`, `maxAmount`, `fromTimestamp`, `toTimestamp`, `cursorTimestamp`, `cursorId`.

Single-record lookups (`id`, `txHash`, `reqId`) are not supported on this endpoint. Use the user-scoped [§12.1](#id-12.1-fetch-transaction-history) or [§12.2](#id-12.2-fetch-transaction-details) endpoints instead.

Response uses the same structure as §12.1 (`data.items[]`, `data.hasNextPage`, `data.nextCursor`). Each item includes the `urId` field so you can identify which user the transaction belongs to.

***

### 12.6 Delegator vault transaction history

Query transactions for the Delegate Vault account associated with your partner. The vault account's `urId` is resolved from your partner configuration (`delegateVaultId`).

| Item    | Value                                                                                            |
| ------- | ------------------------------------------------------------------------------------------------ |
| Method  | `POST`                                                                                           |
| Path    | `/api/delegator/v1/transactions`                                                                 |
| Headers | Partner Auth Headers (user identity headers are ignored; vault identity is resolved from config) |

Request body:

```json
{
  "pageSize": 20
}
```

The request accepts the same fields as [§12.1 Fetch transaction history](#id-12.1-fetch-transaction-history), including pagination cursors, type filters, and single-record lookups (`id`, `txHash`, `reqId`).

Response uses the same structure as §12.1. All returned transactions belong to the vault account.

***

## 13. Webhooks

Webhooks are the asynchronous delivery channel for transaction settlement updates. The Partner should subscribe to the `transaction` event for Off-ramp, FX, On-ramp, Fiat Deposit, and Payout settlement.

> Card-Mode-specific webhooks (post-swipe card transaction, `prefund.balance.alert`) are documented in [API Reference: Card Mode: Crypto Backed](/api-reference/cards/crypto-backed-card#id-5-card-mode-webhooks).

### 13.1 Webhook envelope

```json
{
  "event": "transaction",
  "data": {},
  "timestamp": 1704234567
}
```

### 13.2 Webhook signature verification

UR signs webhook requests with EIP-191.

Partner verification steps:

1. Read the exact **raw request body string** (do not re-serialize).
2. Recover the signer address using the body and `X-Api-Signature`.
3. Accept the event only if the signer address matches the **UR public key** provided out of band.

See [Signature and Verification](/api-reference/signature-and-verify) for the canonical recovery algorithm.

### 13.3 Retry and idempotency

* The Partner should return `HTTP 200` within **10 seconds**.
* UR retries non-200 or timed-out webhook deliveries **up to 3 times**, with a **5-minute interval**.
* Use `data.txHash` or the transaction `data.id` as the idempotency key on the Partner side.

### 13.4 Transaction event

The `transaction` event uses the same transaction data structure as `/api/fma/v1/transactions` ([§12.1](#id-12.1-fetch-transaction-history)). Route by `data.type`; see the type table in [§12.1](#id-12.1-fetch-transaction-history) for the canonical set.

***

## 14. Implementation checklist

Before going live:

* Store both `externalUserId` (Partner side) and `urId` (UR side) after onboarding.
* For user-scoped APIs, send at least one of `X-External-User-Id` or `X-Ur-Id` (sending both is allowed; UR resolves by `X-Ur-Id` first).
* Sign `GET` requests with the raw query string, and sign non-`GET` requests with the raw body, then append `urId:{X-Ur-Id}externalUserId:{X-External-User-Id} {X-Api-Deadline}`.
* Keep `reqId` **stable across retries** for `POST /api/fma/v1/fx-exchange`, `POST /api/fma/v1/internal-transfer`, `POST /api/fma/v1/submit-payout`, `POST /api/fma/v1/onramp`, and `POST /api/fma/v1/onramp-swap`.
* Treat webhook delivery as **at-least-once** and implement idempotency keyed on `data.txHash` or `data.id`.
* Reconcile transaction history through `POST /api/fma/v1/transactions`; use `txHash`, `id`, or `reqId` on that same endpoint for exact lookup.
* Verify every webhook with EIP-191 recovery against UR's public key ([§13.2](#id-13.2-webhook-signature-verification)).
* If the Partner is enabling Card Mode: Crypto Backed, complete the additional checklist in [API Reference: Card Mode: Crypto Backed](/api-reference/cards/crypto-backed-card#id-7-implementation-checklist).

***

## 15. Reference docs

* [Integration Guide](https://docs.ur.app/getting-started/integration-guide): Account Mode, Card Mode, KYC Mode decisions.
* [Core Banking Overview](https://docs.ur.app/concepts/core-banking-overview)
* [Signature and Verification](/api-reference/signature-and-verify)
* [Webhook Reference](https://docs.ur.app/developer-resources/webhook)
* [API Reference: Card Mode: Crypto Backed](/api-reference/cards/crypto-backed-card): the Partner-side integration surface for Card Mode: Crypto Backed (authorization callback, Prefund Account, card-related webhooks).
* [API Reference: External Wallet Access Mode](/api-reference/account/external-wallet-access-mode): the other Account Mode.


# Crypto Backed Card

Prefund operations, card authorization callback, and card webhooks for Crypto Backed mode.

> This document is the Partner-facing API reference for **Card Mode: Crypto Backed**. It covers the three integration surfaces a Partner must implement on top of their chosen Account Mode: **Prefund Account operations**, the **Card Authorization callback** (UR → Partner, synchronous), and the **Card Mode webhooks** (post-swipe and Prefund balance alerts).
>
> For the conceptual definition of Card Mode and how it relates to Account Mode, see [Integration Guide](https://docs.ur.app/getting-started/integration-guide#card-mode-where-card-spend-draws-funds-from). For the Account Mode API surfaces this mode plugs into, see [API Reference: Managed Custody Mode](/api-reference/account/managed-custody-mode) or [API Reference: External Wallet Access Mode](/api-reference/account/external-wallet-access-mode).

***

## 1. Mode context

### 1.1 What Card Mode: Crypto Backed is

**Card Mode** answers a single question: when the user taps the card, **which balance is debited**?

* **Card Mode: Fiat Only**: the card draws exclusively from the user's UR fiat balance. UR runs the authorization on-chain; the Partner is **not** in the authorization path.
* **Card Mode: Crypto Backed** *(this document)*: the card can settle directly against the user's crypto holdings via a Partner-funded **Prefund Account**, with no per-swipe off-ramp required. UR synchronously consults the Partner at authorization time; the Partner asynchronously debits the user's crypto after a successful swipe.

Card Mode is **orthogonal to Account Mode**; a Partner can pair Crypto Backed with either External Wallet Access Mode or Managed Custody Mode.

### 1.2 The two phases

Crypto Backed runs in two phases that operate on different timescales:

| Phase             | Initiator  | Cadence                            | What happens                                                                                                                                                                                                                                                                                                                   |
| ----------------- | ---------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **1. Prefund**    | Partner    | Scheduled (recurring, or on alert) | Partner calls the UR off-ramp contract with USDC, targeting its own Prefund Account. The Prefund Account is credited in the Partner's settlement currency.                                                                                                                                                                     |
| **2. Card spend** | User (tap) | Real-time                          | Mastercard routes the auth to UR → UR calls the Partner webhook → Partner returns a `decision` → UR settles against the Prefund Account (`APPROVE`) or the user's UR fiat balance (`PASS`) → after Mastercard finalizes, UR sends a post-swipe webhook to the Partner → on an `APPROVE`, the Partner debits the user's crypto. |

The Prefund Account exists to absorb real-time card authorizations: Partners cannot bridge user crypto fast enough to meet UR's 1 s total authorization window, so UR is pre-funded and the Partner reconciles asynchronously.

### 1.3 What this document covers vs. doesn't

**This document covers** Partner-facing surfaces that exist only in Card Mode: Crypto Backed:

* [§3 Prefund Account](#id-3-prefund-account) funding flow + balance API
* [§4 Card Authorization callback (UR → Partner, synchronous)](#id-4-card-authorization-callback-ur-greater-than-partner)
* [§5 Card Mode webhooks](#id-5-card-mode-webhooks) (post-swipe transaction + Prefund balance alert)
* [§6 Implementation checklist](#id-6-implementation-checklist)

**This document does NOT cover** endpoints that are common to both Card Modes (Create Card, Get Card Info, Set Default Currency, Card history). Those live in the Account Mode API reference under the Card section:

* Managed Custody Partners → [API Reference: Managed Custody Mode §11](/api-reference/account/managed-custody-mode#id-11-card)
* External Wallet Access Partners → [API Reference: External Wallet Access Mode](/api-reference/account/external-wallet-access-mode#id-3-card)

### 1.4 Partner prerequisites

Before integrating Card Mode: Crypto Backed, the Partner must have:

* An Account Mode chosen and operational (External Wallet Access or Managed Custody).
* **KYB (Know Your Business) completed with UR.** UR requires KYB before it provisions your Prefund Account.
* The ability to make an **initial USDC off-ramp** into the Prefund Account to seed card spending capacity.
* The ability to **schedule recurring off-ramp calls** that top up the Prefund Account on a supported chain.
* The ability to respond to the Card Authorization callback within **500 ms** with valid Partner Auth signatures.
* The ability to **reliably debit user crypto after the swipe is approved**, typically via a smart contract wallet under the Partner's programmatic control, or centralized custody, so the user cannot move the crypto between approval and debit. UR does not enforce this on the Partner's behalf.
* Operational tolerance for **working-float management**; if the Prefund Account drains below the minimum balance, authorizations begin to decline.

***

## 2. API foundation

This document inherits the API foundation defined in the Account Mode reference: base URLs, Partner Auth (EIP-191) headers, response envelope, idempotency rules. See:

* Managed Custody Partners → [API Reference: Managed Custody Mode §2](/api-reference/account/managed-custody-mode#id-2-api-foundation)
* External Wallet Access Partners → [API Reference: External Wallet Access Mode](/api-reference/account/external-wallet-access-mode#id-1-overview)

Two header-block shorthands are used below:

* **Partner-Scoped Partner Auth Headers**: `X-Api-Signature` + `X-Api-Deadline` + `X-Api-PublicKey`, **no** user identity. Used for Prefund Account endpoints ([§3](#id-3-prefund-account)).
* **UR-Signed Inbound**: for requests where UR calls the Partner ([§4](#id-4-card-authorization-callback-ur-greater-than-partner) callback and [§5](#id-5-card-mode-webhooks) webhooks), UR signs the body with EIP-191 using UR's signer key. The Partner must verify the signature against UR's public key before acting on the payload.

***

## 3. Prefund Account

> **Scope:** Partner-scoped (not user-scoped). One Prefund Account per Partner, regardless of how many users the Partner has onboarded. A single Prefund Account can hold a balance in more than one settlement currency.

### 3.1 What the Prefund Account is

The Prefund Account is a Partner-level operational funding account used to settle card swipes that the Partner approves with `decision: APPROVE`. The Partner funds it; UR debits it during card settlement; the Partner asynchronously debits the user's crypto to replenish its own books.

It is **not**:

* A user balance; users cannot see it, cannot deposit to it, and cannot withdraw from it.
* A real-time authorization gate the Partner queries per swipe. Use it for **operational monitoring only**; UR performs Prefund availability checks inside the Card Authorization flow ([§4](#id-4-card-authorization-callback-ur-greater-than-partner)).

### 3.2 Funding the Prefund Account

You fund the Prefund Account yourself, by calling the UR off-ramp contract with your Prefund Account as the off-ramp target account. UR does not run an operator-mediated deposit step, and there is no separate deposit address to send crypto to.

```mermaid
sequenceDiagram
    participant Partner as Partner Treasury
    participant ORC as UR Off-ramp Contract
    participant Pre as Prefund Account<br/>(Partner-scoped)

    Note over Partner, Pre: Scheduled top-up (recurring, or triggered by a balance alert)
    Partner->>ORC: 1. Call off-ramp with USDC, target = your Prefund Account
    ORC-->>Pre: 2. Credit the Prefund Account in your settlement currency
```

**Currently supported:**

* **Off-ramp USDC on supported chains:** call the off-ramp contract with USDC on any UR [Supported Chain](https://docs.ur.app/api-reference/cards/pages/0KrXzznVQkBgtZweDaXH#id-3.1.9-get-supported-chain-config). USDC is the only deposit currency today.
* **Settlement currency:** the currency your Prefund Account is held in, on Mantle, as a tokenized fiat balance. It defaults to `USD`. To use a different settlement currency, agree it with UR **before** onboarding; the settlement currency you pick affects your off-ramp pricing.
* **Minimum balance:** agreed with UR during onboarding, **per settlement currency**. UR emits `prefund.balance.alert` ([§5.5](#id-5.5-prefund-balance-alert-event-prefund.balance.alert)) when a currency's balance reaches or falls below its threshold.

Operational constraints:

* **Off-ramp to your own Prefund Account, never to a user account.** Confirm your Prefund Account details with UR before your first production top-up, and use them as the off-ramp target account.
* **Do not assume an off-ramp is instantly reflected.** The off-ramp settles asynchronously; monitor the Prefund Balance API ([§3.3](#id-3.3-get-prefund-balance)) and the `prefund.balance.alert` webhook ([§5.5](#id-5.5-prefund-balance-alert-event-prefund.balance.alert)) rather than assuming the credit has landed.
* **Maintain a buffer above the minimum balance.** Each `prefund.balance.alert` indicates the Partner is now exposed to authorization decline risk on the next swipe. Plan top-up SLA accordingly.

### 3.3 Get Prefund balance

| Item    | Value                               |
| ------- | ----------------------------------- |
| Method  | `GET`                               |
| Path    | `/api/fma/v1/prefund-balance`       |
| Headers | Partner-Scoped Partner Auth Headers |

This endpoint is **partner-scoped**, not user-scoped. It returns the current state of the Partner's Prefund Account.

Request body: none. Query parameters: none.

Response example:

The response is a **flat, single-currency** object describing the Prefund Account's settlement currency (defaults to `USD`):

```json
{
  "code": 0,
  "message": "",
  "data": {
    "partnerId": "partner_id",
    "account": "0xPrefundAccAddress",
    "currency": "USD",
    "balance": "125000.00",
    "minBalance": "50000.00",
    "level": "normal",
    "allowance": "125000.00",
    "updatedAt": 1713700000
  }
}
```

Response fields:

| Field        | Description                                                                                                                                   |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `partnerId`  | Partner identifier registered with UR.                                                                                                        |
| `account`    | On-chain address of the Prefund Account (Mantle).                                                                                             |
| `currency`   | The settlement currency, agreed with UR (defaults to `USD`, see [§3.2](#id-3.2-funding-the-prefund-account)).                                 |
| `balance`    | Current balance, decimal string.                                                                                                              |
| `minBalance` | Minimum balance threshold. `prefund.balance.alert` fires when the balance reaches or falls below it.                                          |
| `level`      | Balance level relative to `minBalance`. This endpoint emits `"normal"` (balance > `minBalance`) or `"warning"` (balance ≤ `minBalance`) only. |
| `allowance`  | The card-spend contract allowance for this account, as a human-readable amount.                                                               |
| `updatedAt`  | Unix seconds, last balance update.                                                                                                            |

> This endpoint returns a single currency and does **not** include a `settleMode` field. Record your configured settlement mode from onboarding; it is not read back from this response.

Usage recommendations:

* **Polling cadence**: operational dashboard cadence, e.g. no more frequently than once per minute unless UR advises otherwise.
* **Authoritative source**: treat this endpoint as the source of truth for `balance` and `minBalance`. Webhook payloads are eventually consistent.
* **Reactive pattern**: use `prefund.balance.alert` ([§5.5](#id-5.5-prefund-balance-alert-event-prefund.balance.alert)) as a passive trigger; call this endpoint to confirm current state before initiating a top-up.
* **Not for authorization decisions**: do not call this endpoint from inside the Card Authorization callback ([§4](#id-4-card-authorization-callback-ur-greater-than-partner)). UR already performs Prefund availability checks before approving. Adding a Partner-side check increases your 500 ms callback latency for no benefit.

***

## 4. Card authorization callback (UR → Partner)

> **Scope:** Required only for Card Mode: Crypto Backed. Card Mode: Fiat Only Partners are not in the authorization path at all.

When the user taps the card, UR **synchronously** calls a Partner-hosted authorization endpoint. The Partner returns an approval decision and the currency to settle in; UR then performs final balance, limit, compliance, and Prefund checks before responding to Mastercard.

### 4.1 Settlement modes

Partners configure one of two settlement modes during onboarding. The mode determines what UR sends in the authorization request and what the Partner is expected to return.

| Mode                   | `settleMode`         | Description                                                                                                                                                                                                                                                                                                                                              |
| ---------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Fixed**              | `fixed` (default)    | UR pre-computes the exact amount it will debit from the Prefund Account, in the Partner's configured settlement currency, including FX spread and interchange. UR sends that amount in the `settlement` field of the authorization request. The Partner approves or declines against that pre-calculated amount, and does not choose the debit currency. |
| **Partner Controlled** | `partner_controlled` | The Partner chooses which Prefund settlement currency to debit on each swipe. UR sends the raw Mastercard transaction and settlement details, and the Partner returns the chosen currency in `paidCurrency`. Because UR does not know the currency in advance, it cannot pre-compute the debit amount; the Partner sizes it.                             |

**Fixed mode** is designed for Partners who hold a single-currency Prefund Account and want UR to handle all FX conversion. The Partner does not need to perform any currency conversion; the `settlement.amount` in the request is the exact amount that will be debited from the Prefund Account.

### 4.2 Endpoint contract

The Partner hosts an endpoint at a URL registered with UR during onboarding (e.g. `https://api.partner.example/ur/card-authorizations`). UR calls it on every card authorization for users whose accounts are configured with Card Mode: Crypto Backed.

| Item           | Value                                                                                                                                                         |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Method         | `POST`                                                                                                                                                        |
| Path           | Partner-hosted (registered with UR during onboarding)                                                                                                         |
| Direction      | **UR → Partner**                                                                                                                                              |
| Auth           | Mutual EIP-191: UR signs the request body; Partner signs the response body. Both parties verify the other's signature against pinned addresses.               |
| Latency budget | **≤ 500 ms** Partner response time. The Partner's 500 ms sits inside UR's 1 s total authorization window; UR uses the remaining \~500 ms for internal checks. |
| Failure mode   | Timeout / 5xx / signature-verification failure / invalid response → UR treats as **DECLINE** for this swipe.                                                  |

The Partner's authorization endpoint should be **low-latency, idempotent, auditable, and fail-safe**. Treat the 500 ms budget as a hard ceiling; design for p99 well under it.

### 4.3 Flow

```mermaid
sequenceDiagram
    participant MC as Mastercard Network
    participant UR as UR
    participant Partner as Partner Backend
    participant Pool as Prefund Account
    participant UserBal as User UR Fiat Balance

    MC->>UR: 1. Authorization request (user taps card)
    Note over UR: 2. Build signed authorization envelope
    UR->>Partner: 3. POST signed { event, data: { requestId, action, transaction, settlement, merchant, ... } }
    Note over Partner: 4. Verify UR signature, evaluate authorization
    Partner-->>UR: 5. Signed { requestId, decisionId, decision, paidCurrency }
    Note over UR: 6. Verify Partner signature, apply decision
    UR->>UR: 7. Final checks: balance, limit, compliance, Prefund availability
    alt decision = APPROVE AND UR checks pass
      UR->>Pool: 8a. Book against Prefund Account in paidCurrency
      UR-->>MC: 9. APPROVED (within 1 s)
    else any decline
      UR-->>MC: 9. DECLINED
    end
    Note over UR, Partner: Result follow-up (§5.4)
    UR->>Partner: 10. transaction_v2 webhook (MARQETA_AUTHORIZE)
    opt Prefund Account was used and approved
      Partner->>Partner: 11. Debit equivalent crypto from user
    end
```

### 4.4 Request signing (UR → Partner)

UR signs every authorization request body with EIP-191 using UR's server private key. The signature and signer address are sent in HTTP headers:

| Header            | Description                                                                                                  |
| ----------------- | ------------------------------------------------------------------------------------------------------------ |
| `X-Api-Signature` | EIP-191 signature over the raw JSON request body (`0x`-prefixed hex, 65 bytes).                              |
| `X-Api-PublicKey` | UR's signer Ethereum address. The Partner **must** verify this matches the address pinned during onboarding. |
| `X-Request-Id`    | Matches `data.requestId` in the body. Convenience header for logging.                                        |

Verification: recover the signer address from `EIP191(body)` using `X-Api-Signature`, then assert it equals the pinned UR public key. Reject if it doesn't match.

### 4.5 Request body (UR → Partner)

```json
{
  "event": "card_authorization",
  "timestamp": 1713700000,
  "data": {
    "version": "1",
    "requestId": "auth-req-uuid-001",
    "expiresAt": 1713700500,
    "action": "AUTHORIZE",
    "partnerId": "partner_example",
    "userId": "7123456789",
    "authorizationToken": "marqeta-auth-token-001",
    "transaction": {
      "currency": "SGD",
      "amount": "10000"
    },
    "settlement": {
      "currency": "USD",
      "amount": "7450"
    },
    "merchant": {
      "id": "merchant-123",
      "name": "Coffee Shop",
      "mcc": "5812",
      "city": "Singapore",
      "country": "SG"
    }
  }
}
```

Field reference:

| Field                       | Type   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| --------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event`                     | string | Always `"card_authorization"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `timestamp`                 | number | Unix seconds when the request was built.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `data.version`              | string | Protocol version. Currently `"1"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `data.requestId`            | string | UR-generated idempotency key. The Partner **must** echo this back in the response and respond identically to retries with the same `requestId`.                                                                                                                                                                                                                                                                                                                                                                                                       |
| `data.expiresAt`            | number | Unix milliseconds. The Partner should reject requests where `expiresAt < now`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `data.action`               | string | `"AUTHORIZE"` for initial authorization, `"INCREMENT"` for an incremental auth on the same transaction.                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `data.partnerId`            | string | Partner identifier.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `data.userId`               | string | User's UR ID.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `data.authorizationToken`   | string | Card network authorization token.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `data.transaction.currency` | string | Merchant's transaction currency (ISO 4217 alpha code, e.g. `"SGD"`).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `data.transaction.amount`   | string | Transaction amount in the transaction currency (integer string in the token's smallest unit).                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `data.settlement.currency`  | string | Settlement currency symbol (e.g. `"USD"`, `"EUR"`). In **fixed mode**, this is always the Partner's configured `settleCurrency`. In **partner\_controlled mode**, this is the Mastercard network settlement currency.                                                                                                                                                                                                                                                                                                                                 |
| `data.settlement.amount`    | string | Settlement amount. **Its meaning depends on your `settleMode`; read it accordingly.** In **fixed mode**, this is the **pre-computed Prefund debit amount**, including FX spread and interchange. It is the exact amount UR debits from your Prefund Account if you approve, so you can check it against your balance directly. In **partner\_controlled mode**, this is the **Mastercard network settlement amount**. It is **not** your Prefund debit; UR applies FX and interchange at execution, so convert it yourself before you size the debit. |
| `data.merchant.*`           | object | Merchant details from the card network.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |

### 4.6 Response body (Partner → UR)

The Partner **must** sign the response body with EIP-191 and include signature headers:

| Header            | Description                                                                                                 |
| ----------------- | ----------------------------------------------------------------------------------------------------------- |
| `X-Api-Signature` | EIP-191 signature over `body + " " + deadline` (the response body bytes, a space, and the deadline string). |
| `X-Api-Deadline`  | Unix seconds expiry for the response signature (e.g. 60 s from now).                                        |
| `X-Api-PublicKey` | Partner's signer Ethereum address.                                                                          |

**Approval:**

```json
{
  "requestId": "auth-req-uuid-001",
  "decisionId": "partner-decision-uuid-001",
  "decision": "APPROVE",
  "paidCurrency": "USD"
}
```

**Decline:**

```json
{
  "requestId": "auth-req-uuid-001",
  "decisionId": "partner-decision-uuid-002",
  "decision": "DECLINE",
  "paidCurrency": ""
}
```

**Pass (fall back to user's own balance):**

```json
{
  "requestId": "auth-req-uuid-001",
  "decisionId": "partner-decision-uuid-003",
  "decision": "PASS",
  "paidCurrency": ""
}
```

Response field reference:

| Field          | Required   | Description                                                                                                                                                                                                                                                                                                                                                                                                                 |
| -------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `requestId`    | Yes        | Must match `data.requestId` from the request. UR rejects mismatches.                                                                                                                                                                                                                                                                                                                                                        |
| `decisionId`   | Yes        | Partner-generated unique ID for this decision. Used for audit trail and reconciliation.                                                                                                                                                                                                                                                                                                                                     |
| `decision`     | Yes        | `"APPROVE"`, `"DECLINE"`, or `"PASS"`. Any other value is treated as an error (UR declines).                                                                                                                                                                                                                                                                                                                                |
| `paidCurrency` | On APPROVE | The currency to debit from the funding source that `decision` selected (ISO symbol, e.g. `"USD"`, `"EUR"`). On `APPROVE`, it names a currency in your **Prefund Account**. On `PASS`, it names a currency in the **user's UR fiat balance**; leave it empty to let UR select the currency for you. Not used on `DECLINE`. See the mode tables in [§4.6 examples](#examples-by-settlement-mode) for every valid combination. |

#### Decision semantics

| `decision` | Effect                                                                                                                                                                                                                   |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `APPROVE`  | Settle against the **Prefund Account** in `paidCurrency`. UR still performs final checks and may override to DECLINE.                                                                                                    |
| `DECLINE`  | Reject the authorization entirely. The card swipe is declined.                                                                                                                                                           |
| `PASS`     | Skip double booking for this swipe. UR falls back to the **user's own UR fiat balance**; the swipe behaves as if the user were on Card Mode: Fiat Only. No Prefund debit occurs; no Partner-side crypto debit is needed. |

`PASS` is useful when the Partner wants to selectively route certain transactions to the user's own balance; for example, small transactions below a threshold, or when the user has sufficient fiat balance and the Partner prefers not to consume Prefund capacity.

Important behavior:

* **UR retains final authority**: even after an `APPROVE`, UR performs balance / limit / compliance / Prefund availability checks and can still decline.
* **Increment consistency**: for `action: INCREMENT`, the Partner must return the same `decision` and `paidCurrency` as the original `AUTHORIZE` for the same transaction. UR rejects currency changes mid-transaction. If the original was `PASS`, the increment must also be `PASS`.
* **Fixed mode simplification**: in fixed mode, the `settlement.amount` in the request already includes FX fees. The Partner only needs to check if the Prefund Account can absorb that amount and approve or decline. `PASS` is also available in fixed mode if the Partner wants to route specific transactions to the user's balance.

#### Examples by settlement mode

Your `settleMode` changes only what you may return on `APPROVE`. `PASS` and `DECLINE` behave identically in both modes. The following tables list every valid combination.

**Fixed mode.** UR pre-computes the debit, so you never run a currency conversion:

| `decision` | `paidCurrency`                                  | What UR does                                                                                                  |
| ---------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `APPROVE`  | Equal to `settlement.currency` from the request | Debits `settlement.amount` from your Prefund Account. That amount already includes FX spread and interchange. |
| `APPROVE`  | Any other currency                              | Declines the swipe. In fixed mode you cannot choose the debit currency.                                       |
| `APPROVE`  | Empty                                           | Declines the swipe. `paidCurrency` is required on `APPROVE`.                                                  |
| `PASS`     | Empty                                           | Settles from the user's UR fiat balance and selects the currency for you. Your Prefund Account is untouched.  |
| `PASS`     | A currency the user holds                       | Settles from that currency of the user's UR fiat balance. Declines if that balance is short.                  |
| `DECLINE`  | Empty                                           | Declines the swipe.                                                                                           |

**Partner controlled mode.** You choose the Prefund currency, so UR cannot pre-compute the debit:

| `decision` | `paidCurrency`                                | What UR does                                                                                                 |
| ---------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `APPROVE`  | Any currency your Prefund Account holds       | Debits that currency from your Prefund Account. UR applies FX spread and interchange at execution.           |
| `APPROVE`  | A currency your Prefund Account does not hold | Declines the swipe.                                                                                          |
| `APPROVE`  | Empty                                         | Declines the swipe. `paidCurrency` is required on `APPROVE`.                                                 |
| `PASS`     | Empty                                         | Settles from the user's UR fiat balance and selects the currency for you. Your Prefund Account is untouched. |
| `PASS`     | A currency the user holds                     | Settles from that currency of the user's UR fiat balance. Declines if that balance is short.                 |
| `DECLINE`  | Empty                                         | Declines the swipe.                                                                                          |

### 4.7 Fail-safe semantics

UR treats the following as **DECLINE** (the Partner's authorization endpoint is considered unavailable):

| Failure                                                               | UR action                                                                             |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| HTTP timeout (no response within 500 ms)                              | Decline with internal reason `partner_timeout`                                        |
| HTTP 5xx                                                              | Decline with internal reason `partner_5xx`                                            |
| HTTP 4xx                                                              | Decline with internal reason `partner_4xx` (likely Partner config error; investigate) |
| Invalid or missing signature on response                              | Decline with internal reason `partner_sig_invalid`                                    |
| `requestId` mismatch                                                  | Decline with internal reason `partner_invalid_response`                               |
| Missing `decisionId`                                                  | Decline with internal reason `partner_invalid_response`                               |
| Invalid `decision` value                                              | Decline with internal reason `partner_invalid_response`                               |
| `decision: APPROVE` but `paidCurrency` missing                        | Decline with internal reason `partner_invalid_response`                               |
| Unsupported `paidCurrency`                                            | Decline with internal reason `partner_invalid_response`                               |
| Response body exceeds 64 KB                                           | Decline with internal reason `partner_invalid_response`                               |
| Partner signature deadline expired or too far in the future (> 5 min) | Decline with internal reason `partner_sig_invalid`                                    |

UR exposes the internal decline reason on the post-swipe transaction webhook ([§5.4](#id-5.4-transaction-event-post-swipe-event-transaction-data.type-crd)) for reconciliation.

**Operational note on the authorization URL.** The Partner-hosted authorization URL is registered with UR during onboarding. To update it (e.g. environment migration), coordinate the change with UR; there is no self-serve endpoint for this.

### 4.8 Recommended `reason` codes

> For Partners who include a `reason` field in their response (optional, for reconciliation/analytics).

| `reason`                       | When to use                                           |
| ------------------------------ | ----------------------------------------------------- |
| `ok`                           | Approve.                                              |
| `insufficient_user_crypto`     | User does not have enough crypto on the Partner side. |
| `insufficient_partner_prefund` | Partner's Prefund Account is too low.                 |
| `user_blocked`                 | Partner-side compliance block.                        |
| `merchant_blocked`             | Partner-side MCC / merchant block.                    |
| `card_disabled`                | Card is locked or frozen on the Partner side.         |
| `internal_error`               | Catch-all decline; treat as transient.                |

### 4.9 Fixed mode: FX quote details

In **fixed mode**, UR pre-calculates the `settlement.amount` using on-chain FX rates from the card authorization contract. The calculation mirrors the contract's actual debit logic to ensure the quoted amount matches the on-chain settlement:

* **Same-currency** (e.g. Prefund is USD, settlement is USD): `settlement.amount` = network settlement amount, no FX.
* **Cross-currency** (e.g. Prefund is EUR, settlement is USD): UR calls the contract's `getRate` and `getSpread` view functions to compute the exact amount including FX spread and interchange fees.

The quoted amount is deterministic at the time of the authorization request. Between the quote and the on-chain settlement (typically < 1 second), exchange rates may shift marginally. This is operationally acceptable for the card authorization use case.

Partners in fixed mode do **not** need to perform any FX calculation; the `settlement.amount` is ready to use for balance checks and ledger entries.

***

## 5. Card Mode webhooks

UR delivers Card Mode events via the standard webhook envelope. The Partner must verify every webhook signature before acting on it.

### 5.1 Webhook envelope

```json
{
  "event": "<event-type>",
  "data": {},
  "timestamp": 1704234567
}
```

### 5.2 Signature verification

UR signs webhook bodies with EIP-191. Verification steps:

1. Read the **exact raw request body string** (do not re-serialize).
2. Recover the signer address using the body and `X-Api-Signature`.
3. Accept the event only if the signer address matches the **UR public key** provided out of band.

See [Signature and Verification](/api-reference/signature-and-verify) for the canonical recovery algorithm.

### 5.3 Retry and idempotency

* The Partner should return `HTTP 200` within **10 seconds**.
* UR retries non-200 / timed-out webhook deliveries **up to 3 times**, with a **5-minute interval**.
* Use `data.id` as the idempotency key for card payment `transaction_v2` events.

### 5.4 Card payment transaction (`event: "transaction_v2"`)

UR sends `transaction_v2` when a card payment transaction reaches a terminal state. Card payment records use `data.type = "MARQETA_AUTHORIZE"`. The same event covers successful and failed card payments.

Confirmed payment example:

```json
{
  "event": "transaction_v2",
  "data": {
    "id": 353244,
    "txHash": "0x30e3...",
    "blockNumber": 39376686,
    "createTimeE9": 1780298041358000000,
    "broadcastTimeE9": 1780298041358000000,
    "finalTimeE9": 1780298045000000000,
    "type": "MARQETA_AUTHORIZE",
    "chainId": "eip155:5003",
    "chainName": "MNT",
    "urId": "6570621322",
    "direction": "OUT",
    "amount": "0.06",
    "currency": "usd",
    "status": "CONFIRMED",
    "detailsJson": "{\"authorizationId\":\"card-auth-0001\",\"balance\":\"12360572.18\",\"cardCurrency\":\"USD\",\"cardId\":\"card-id-0001\",\"city\":\"Singapore\",\"country\":\"SGP\",\"mcc\":\"7299\",\"merchant\":\"Merchant ABC\",\"merchantId\":\"merchant-0001\",\"settlementAmount\":\"0.06\",\"settlementCurrency\":\"EUR\",\"transactionAmount\":\"0.06\",\"transactionCurrency\":\"USD\"}"
  },
  "timestamp": 1780298051
}
```

Failed payment example:

```json
{
  "event": "transaction_v2",
  "data": {
    "id": 353245,
    "txHash": "0x9eec...",
    "blockNumber": 96571332,
    "createTimeE9": 1781272976000000000,
    "broadcastTimeE9": 1781272976000000000,
    "finalTimeE9": 1781272976000000000,
    "type": "MARQETA_AUTHORIZE",
    "chainId": "eip155:5000",
    "chainName": "MNT",
    "urId": "4596332147",
    "direction": "OUT",
    "amount": "15.39",
    "currency": "cnh",
    "status": "FAILED",
    "detailsJson": "{\"authorizationId\":\"card-auth-0002\",\"merchantId\":\"merchant-0002\",\"merchant\":\"Merchant ABC\",\"mcc\":\"5999\",\"city\":\"Shanghai\",\"country\":\"CHN\",\"cardId\":\"card-id-0002\",\"cardCurrency\":\"USD\",\"transactionCurrency\":\"CNH\",\"settlementCurrency\":\"EUR\",\"transactionAmount\":\"15.39\",\"settlementAmount\":\"1.97\",\"reason\":\"Insufficient USD24 allowance\"}"
  },
  "timestamp": 1781272979
}
```

Key fields for Crypto Backed reconciliation:

| Field                 | Description                                                                                    |
| --------------------- | ---------------------------------------------------------------------------------------------- |
| `id`                  | UR transaction record ID. Use this field as the idempotency key for `transaction_v2` delivery. |
| `type`                | Card payment transactions use `MARQETA_AUTHORIZE`.                                             |
| `status`              | Terminal transaction status, such as `CONFIRMED` or `FAILED`.                                  |
| `direction`           | `OUT` for card spend.                                                                          |
| `amount` / `currency` | Amount and currency recorded on the transaction row.                                           |
| `txHash`              | On-chain transaction hash when available.                                                      |
| `detailsJson`         | Stringified JSON with card payment details. Parse this value before reading nested fields.     |

`detailsJson` may include the following card payment fields:

| Field                 | Description                                                                             |
| --------------------- | --------------------------------------------------------------------------------------- |
| `authorizationId`     | Card authorization identifier.                                                          |
| `balance`             | Balance value recorded with the card payment.                                           |
| `cardCurrency`        | Card currency.                                                                          |
| `cardId`              | Card identifier.                                                                        |
| `city`                | Merchant city.                                                                          |
| `country`             | Merchant country.                                                                       |
| `mcc`                 | Merchant category code.                                                                 |
| `merchant`            | Merchant display name.                                                                  |
| `merchantId`          | Merchant identifier.                                                                    |
| `settlementAmount`    | Settlement amount recorded for the card payment.                                        |
| `settlementCurrency`  | Settlement currency recorded for the card payment.                                      |
| `transactionAmount`   | Merchant transaction amount.                                                            |
| `transactionCurrency` | Merchant transaction currency.                                                          |
| `reason`              | Failure reason. This field appears only when the card payment record includes a reason. |

**Partner crypto-debit responsibility.** When a Crypto Backed card payment succeeds, the partner is responsible for computing and debiting the corresponding user crypto amount in its own system. UR reports the card payment transaction data; UR does not move user crypto on the partner's behalf.

Use `transaction_v2` records with `data.type = "MARQETA_AUTHORIZE"` and `data.status = "FAILED"` for failed card payments. The failure reason is in `detailsJson.reason` when UR has one.

### 5.5 Prefund balance alert (`event: "prefund.balance.alert"`)

UR emits `prefund.balance.alert` when the balance of one settlement currency in your Prefund Account crosses a threshold. Use it as a passive trigger to top up before authorizations start to decline.

The alert is **per currency**, not per account. If two currencies cross a threshold, UR sends two events.

Example:

```json
{
  "event": "prefund.balance.alert",
  "data": {
    "partnerId": "partner_id",
    "account": "0xPrefundAccAddress",
    "currency": "EUR",
    "balance": "18000.00",
    "minBalance": "20000.00",
    "previousLevel": "normal",
    "level": "warning",
    "updatedAt": 1713700000
  },
  "timestamp": 1713700005
}
```

Fields:

| Field           | Description                                                            |
| --------------- | ---------------------------------------------------------------------- |
| `partnerId`     | Partner identifier registered with UR.                                 |
| `account`       | On-chain address of the Prefund Account (Mantle).                      |
| `currency`      | The settlement currency this alert is about.                           |
| `balance`       | Balance in that currency at the moment the level changed.              |
| `minBalance`    | The threshold configured for that currency.                            |
| `previousLevel` | The level before this change.                                          |
| `level`         | The level after this change: `"normal"`, `"warning"`, or `"critical"`. |
| `updatedAt`     | Unix seconds, the balance update that triggered the alert.             |

**When UR sends it.** The alert is edge-triggered: UR sends it only when `level` **changes** for a currency, not repeatedly while the balance sits below the threshold. You therefore receive an alert on the way down (`normal` to `warning`, `warning` to `critical`) and again on recovery (back to `normal`), and nothing in between.

**How to handle it:**

* Treat the alert as a **trigger, not as truth**. Call [Get Prefund balance](#id-3.3-get-prefund-balance) to confirm the current state before you top up; webhook payloads are eventually consistent.
* Top up **that currency**. A `warning` on one currency says nothing about the others.
* Idempotency key: `account` + `currency` + `updatedAt`. Retry and delivery semantics follow [§5.3](#id-5.3-retry-and-idempotency).
* A `critical` level means the next swipe in that currency is at real risk of declining. Treat it as a page, not a ticket.

***

## 6. Implementation checklist

Before going live with Card Mode: Crypto Backed:

**Prefund operations**

* Confirm the off-ramp chain, the deposit currency, your settlement currency, and the minimum balance (all agreed during onboarding).
* Confirm your Prefund Account details with UR, and use them as the off-ramp target account.
* Implement recurring off-ramp automation with an SLA aligned to your minimum-balance buffer.
* Build an operational dashboard that polls `GET /prefund-balance` and surfaces `level` transitions.
* Subscribe to `prefund.balance.alert` ([§5.5](#id-5.5-prefund-balance-alert-event-prefund.balance.alert)) and route it to your ops channel. Alerts are per currency.

**Card Authorization callback**

* Host the authorization endpoint at the URL registered with UR (see the operational note in [§4.7](#id-4.7-fail-safe-semantics)).
* Verify every inbound request's EIP-191 signature against UR's public key before doing any business logic.
* Implement the handler with a **p99 < 500 ms** budget. Pre-compute everything you can; avoid cross-region database calls in the hot path.
* Decide `decision` deterministically. Return `APPROVE` to settle from your Prefund Account, `PASS` to fall back to the user's own UR fiat balance, or `DECLINE` to reject the swipe. See [Examples by settlement mode](#examples-by-settlement-mode) for every valid `decision` and `paidCurrency` combination.
* Make the handler **idempotent on `requestId`**. UR may retry on transient network errors, and you must return the same decision for the same `requestId`.
* Build a fail-safe path: if any internal dependency fails, return `decision: "DECLINE"` with a `reason` rather than a 5xx. UR treats a 5xx as a decline anyway, but an explicit decline preserves the `reason` for reconciliation.

**Post-swipe + reconciliation**

* Subscribe to the `transaction_v2` webhook (§5.4) and verify its signature.
* When you returned `APPROVE` and the swipe settled against your Prefund Account, perform the user-side crypto debit once the `transaction_v2` webhook reports `status: CONFIRMED`. Record it against the same `requestId`.
* Handle the override case: you returned `decision: APPROVE`, but UR still declined the swipe. Clean up any speculative Partner-side state.
* Decide your refund policy and document it for end users.

**Operational**

* Treat webhook delivery as **at-least-once** and implement idempotency on `data.id` (transactions) and `account + currency + updatedAt` (Prefund alerts).
* Maintain monitoring on Partner-side authorization handler latency, success rate, and decline reason distribution.

***

## 7. Reference docs

* [Integration Guide](https://docs.ur.app/getting-started/integration-guide): Account Mode × Card Mode framing.
* [API Reference: Managed Custody Mode](/api-reference/account/managed-custody-mode): common card endpoints (Create Card, Get Card Info, Set Default Currency, history) and the Account Mode this plugs into.
* [API Reference: External Wallet Access Mode](/api-reference/account/external-wallet-access-mode): the alternative Account Mode this can pair with.
* [Signature and Verification](/api-reference/signature-and-verify): EIP-191 signing and verification for Partner Auth and webhook signatures.


# Managed Custody SDK KYC

Onboard users with KYC completed on the user's device via the UR-issued Sumsub access token.

This page documents the alternative onboarding path for Managed Custody Mode where the user completes KYC on their own device using UR's Sumsub tenant. Your backend exchanges the user's URID for a short-lived Sumsub access token, and the Sumsub mobile or web SDK drives the KYC workflow directly with the user.

Use this path when you do not run a Sumsub tenant of your own and you want UR to host the KYC vendor relationship end to end. For the alternative path where your platform completes KYC in your own Sumsub tenant and shares the applicant with UR through Sumsub reuse, see [API reference: Managed Custody Mode](/api-reference/account/managed-custody-mode).

{% hint style="info" %}
This onboarding path requires your `partnerId` to be configured for SDK mode in UR Nacos. Coordinate with your dedicated integration channel before pointing production traffic at it.
{% endhint %}

## 1. Where this fits

Managed Custody Mode supports two KYC onboarding paths. Both produce the same end state (the user's URID is minted, the UR-managed wallet is provisioned, and the bank account is activated). They differ only in who runs the Sumsub workflow.

|                              | Sumsub reuse (share token)                                                                                | Sumsub SDK in your app (this page)                                                                                                            |
| ---------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Sumsub tenant                | Your platform                                                                                             | UR                                                                                                                                            |
| Who runs KYC UX              | Your platform                                                                                             | Sumsub SDK in your app, on the user device                                                                                                    |
| KYC data lands at UR via     | Sumsub share token + `/kyc/check` polling                                                                 | Sumsub webhooks (server to server)                                                                                                            |
| Partner backend calls        | `/create-account` (with `applicantId`), `/kyc/check`, `/kyc/form-a-info`, `/kyc/sign-form`, `/kyc/submit` | `/create-account` (no `applicantId`), `/kyc/sumsub-access-token`, `/kyc/session/current`, `/kyc/form-a-info`, `/kyc/sign-form`, `/kyc/submit` |
| Onboarding state shape       | Coarse (`PartnerDataIngestion`, `IdentityVerification`, `SignFormA`, `Register`)                          | Fine-grained (eight steps; first is `ConfirmYourCountryOfResidence`, last before submit is `SignFormA`)                                       |
| Retry on KYC rejection       | Your platform restarts in your own Sumsub                                                                 | Restart by calling `/create-account` again. Identity reuse is supported.                                                                      |
| Retry after the user is live | Operations-initiated, see [Retry KYC](/api-reference/kyc-and-kyb/retry-kyc)                               | Operations-initiated, see [Retry KYC](/api-reference/kyc-and-kyb/retry-kyc)                                                                   |

The webhook contract (`fma.account.result`) and all post-onboarding banking APIs are identical to the Sumsub reuse path. Once the user reaches the `Live` state, your integration uses the same Managed Custody Mode endpoints regardless of which KYC path got them there.

## 2. End-to-end sequence

```mermaid
sequenceDiagram
    autonumber
    participant U as End user
    participant PA as Partner app
    participant PB as Partner backend
    participant UR as UR OpenAPI
    participant SS as Sumsub UR tenant

    rect rgb(240,250,255)
    note over PB,UR: Phase 1. Account and Sumsub token (synchronous)
    PB->>UR: POST /api/fma/v1/create-account
    note right of PB: body contains email, nationality, residency, dob, documentExpiry<br/>header X-External-User-Id carries the partner-side user id
    UR-->>PB: 200 sessionId + urId + evmAddress + state ConfirmYourCountryOfResidence

    PB->>UR: POST /api/fma/v1/kyc/sumsub-access-token
    note right of PB: empty body, urId from X-Ur-Id (FMAValidate middleware)
    UR->>SS: Issue access token bound to UR own Sumsub tenant (SumSubClient)
    SS-->>UR: token
    UR-->>PB: 200 token
    PB->>PA: relay token to partner app
    end

    rect rgb(255,250,235)
    note over U,SS: Phase 2. KYC on user device (asynchronous)
    PA->>SS: Launch Sumsub SDK with the token
    loop For each Sumsub level
        U->>SS: Submit Country, Address, ID scan, Liveness
        SS->>UR: Webhook applicantPending or applicantReviewed
    end
    U->>SS: Finish last step
    SS->>UR: Webhook applicantWorkflowCompleted GREEN
    note over UR: UR fetches the full applicant data, stores the encrypted snapshot,<br/>and advances the session to state SignFormA
    end

    rect rgb(245,255,245)
    note over PB,UR: Phase 3. Form A and submit (synchronous)
    PA-)PB: SDK completion signal (or your backend polls session/current)
    PB->>UR: GET /api/fma/v1/kyc/session/current
    UR-->>PB: 200 state SignFormA

    PB->>UR: GET /api/fma/v1/kyc/form-a-info with sessionId
    UR-->>PB: 200 formAVersion + text + textHash
    PB->>U: Display Form A and obtain explicit consent

    PB->>UR: POST /api/fma/v1/kyc/sign-form with sessionId + textHash
    UR-->>PB: 200 signature + signerAddress

    PB->>UR: POST /api/fma/v1/kyc/submit with sessionId
    UR-->>PB: 200 state Submitting
    end

    rect rgb(250,245,255)
    note over UR,PB: Phase 4. Activation (asynchronous)
    note over UR: UR generates KYC PDFs, sends them to the banking partner,<br/>polls until the account is approved, then mints the URID to Live status
    UR->>PB: POST partner webhook URL with fma.account.result status activated
    PB-->>UR: 200 ack
    end

    rect rgb(255,240,240)
    note over SS,PB: Failure. Sumsub returns RED at any level
    SS->>UR: Webhook applicantWorkflowCompleted RED
    note over UR: UR closes the session, releases the identity reservation,<br/>and dispatches a rejection webhook
    UR->>PB: POST partner webhook URL with fma.account.result status rejected
    note over PB: The user may retry by calling /create-account again
    end
```

## 3. Integration walkthrough

{% stepper %}
{% step %}

### Create the account

Call `POST /api/fma/v1/create-account` with the partner-side `X-External-User-Id` header and the user's onboarding data in the body. UR mints the URID, provisions the UR-managed wallet, and creates an onboarding session bound to `sessionId`.

Unlike the Sumsub reuse path, you do not send `applicantId`. UR's Sumsub tenant will assign the applicant id later, once the user actually starts the SDK.

Persist `sessionId`, `urId`, and `evmAddress` before showing onboarding progress to the user.
{% endstep %}

{% step %}

### Request a Sumsub access token

Call `POST /api/fma/v1/kyc/sumsub-access-token` with an empty body. The endpoint sits under the same `FMAValidate` middleware as the rest of `/api/fma/v1/kyc/*`; UR reads `urId` from the `X-Ur-Id` header you already sign on each request, resolves the active session server-side, and returns a Sumsub access token with a fixed 20 minute TTL.

If the user pauses before launching the SDK, request a new token; UR re-issues a fresh token bound to the same Sumsub applicant so the workflow resumes where the user left off.
{% endstep %}

{% step %}

### Launch the Sumsub SDK

Pass the token to your partner app and initialize the Sumsub mobile or web SDK. The user completes Country, Address, ID scan, and Liveness levels on their device. Your backend does not need to call UR during this phase; UR receives Sumsub webhooks server to server.

If the user navigates away mid-flow, refresh the token by calling `/api/fma/v1/kyc/sumsub-access-token` again and relaunch the SDK with the new token.
{% endstep %}

{% step %}

### Wait for KYC completion

Detect SDK completion using one of two patterns:

* Push from your partner app to your backend when the SDK reports done.
* Poll `GET /api/fma/v1/kyc/session/current` from your backend every one to five seconds for up to one minute after the SDK starts.

The session reaches state `SignFormA` once Sumsub delivers a `GREEN` `applicantWorkflowCompleted` webhook and UR has stored the verified KYC snapshot. Sumsub webhook delivery is typically subsecond but the 99th percentile can reach 30 seconds; treat the 60 second polling window as the practical timeout.

If Sumsub returns `RED`, UR fires `fma.account.result` with `status` set to `rejected` and `rejectCode` set to `SUMSUB_REJECTED`. You can offer the user a retry path by starting again at `POST /api/fma/v1/create-account` with the same `X-External-User-Id`.

This restart is a different mechanism from the operations-initiated retry documented on [Retry KYC](/api-reference/kyc-and-kyb/retry-kyc). Restarting onboarding is something your platform does after a failed onboarding, and it re-runs the whole flow from `/create-account`. A retry directive applies to a user who is already onboarded, arrives as `fma.additional_kyc.required`, and is entered with `POST /api/fma/v1/kyc/session/create`, never with `/create-account`.
{% endstep %}

{% step %}

### Render Form A and capture consent

Call `GET /api/fma/v1/kyc/form-a-info?sessionId=...` and display `data.text` to the user exactly as returned. The text includes the user's verified KYC identity and the banking terms; obtaining explicit user consent at this step is a regulatory requirement.

Store `data.textHash` for the next step. UR signs the same text on its side under a session lock to prevent any divergence between what the user saw and what UR signs.
{% endstep %}

{% step %}

### Sign Form A

After the user consents, call `POST /api/fma/v1/kyc/sign-form` with `sessionId` and the `textHash` from the previous step. UR re-renders Form A under a session lock and rejects the call with `FORMA_TEXT_MISMATCH` if the hash no longer matches; that signals the staging data changed underneath you. Recover by re-calling `form-a-info` and resigning with the new hash.

UR's TurnKey wallet signs the Form A text on the user's behalf. The returned `signerAddress` equals the `evmAddress` from the create-account response.
{% endstep %}

{% step %}

### Submit the session

Call `POST /api/fma/v1/kyc/submit` with `sessionId`. UR returns `state: Submitting` with `queued: false`. The session is now owned by UR's background scheduler.

`queued: false` is the success signal for this onboarding path; it indicates that UR can proceed with bank activation immediately without waiting for any further user action. The legacy `queued: true` response shape is reserved for an unrelated penny-transfer path that the SDK flow does not use.
{% endstep %}

{% step %}

### Receive the activation webhook

UR runs the bank activation asynchronously (typical latency 30 seconds to five minutes on testnet, 10 to 60 seconds on mainnet). When the URID reaches `Live` status on chain and the bank account is open, UR delivers `fma.account.result` with `status: activated` to your registered webhook URL.

Your webhook handler must verify the EIP-191 signature (`X-Api-Signature` / `X-Api-Signature-V2`), deduplicate on the `X-Webhook-Request-Id` header, and ACK with HTTP 200. UR retries non-2xx responses with exponential backoff.
{% endstep %}
{% endstepper %}

## 4. API reference

All endpoints below sit under Partner Auth. See [Signature and verify](/api-reference/signature-and-verify) for the EIP-191 signing scheme and the canonical-message construction rules.

| Environment    | Base URL                          |
| -------------- | --------------------------------- |
| **Production** | `https://openapi.ur.app`          |
| **Preview**    | `https://openapi-preview.ur.app`  |
| **Testnet**    | `https://uropenapi-qa.ur-inc.xyz` |

> Confirm the exact base URL set with UR before production rollout.

The examples in this section use the testnet base URL.

### 4.1 Create account

Create the user's URID and the onboarding session. UR provisions a UR-managed wallet and stores the session in state `ConfirmYourCountryOfResidence`.

| Item    | Value                                                          |
| ------- | -------------------------------------------------------------- |
| Method  | `POST`                                                         |
| Path    | `/api/fma/v1/create-account`                                   |
| Headers | Partner Auth headers + `X-External-User-Id` (no `X-Ur-Id` yet) |

Request body:

```json
{
  "email": "user@example.com",
  "nationality": "CHN",
  "residency": "CHN",
  "dob": "1994-01-06",
  "documentExpiry": "2036-05-05"
}
```

Request fields:

| Field            | Required | Description                                                                                                                          |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `email`          | Yes      | The user's email address. Must be unique across UR partners; reusing the same email under a different partner returns `L1_CONFLICT`. |
| `nationality`    | No       | ISO 3166-1 alpha-3 country code. Sending it lets UR fail country gates before the URID is minted.                                    |
| `residency`      | No       | ISO 3166-1 alpha-3 country code of residence.                                                                                        |
| `dob`            | No       | Date of birth in `YYYY-MM-DD` format.                                                                                                |
| `documentExpiry` | No       | Passport or ID expiry date in `YYYY-MM-DD` format.                                                                                   |

Response:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "sessionId": "223d567f-ea11-4629-ba2a-94664edc26f6",
    "urId": 5643568810,
    "evmAddress": "0x7c8173c8Fe47D55b9Ee848e89da1149A52632193",
    "state": "ConfirmYourCountryOfResidence",
    "idempotentReplay": false
  }
}
```

Response fields:

| Field              | Description                                                                                                                                                                                       |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sessionId`        | UUID for this onboarding attempt. Constant for the lifetime of the attempt. Use it in every subsequent KYC endpoint on this page.                                                                 |
| `urId`             | The user's URID, equal to the on-chain NFT token id. Stable for the user's lifetime across all retries.                                                                                           |
| `evmAddress`       | The user's UR-managed wallet address. Form A is signed by this address.                                                                                                                           |
| `state`            | Current session state. `ConfirmYourCountryOfResidence` is the entry state for this onboarding path.                                                                                               |
| `idempotentReplay` | `true` when an active onboarding session already exists for `(partnerId, X-External-User-Id)`. The response returns the existing session unchanged so repeated calls cannot mint duplicate URIDs. |

Rules:

* Repeated calls with the same `X-External-User-Id` while a session is active return the same `sessionId` and set `idempotentReplay` to `true`. The user's URID and wallet address never change across these retries.
* After the active session terminates (success, rejection, or timeout), the next call mints a fresh session with an incremented `retryLevel`. The URID is reused; only the onboarding session is new.

### 4.2 Create Sumsub access token

Issue a short-lived Sumsub access token for the user. The token authenticates the Sumsub SDK against UR's Sumsub tenant.

| Item    | Value                                 |
| ------- | ------------------------------------- |
| Method  | `POST`                                |
| Path    | `/api/fma/v1/kyc/sumsub-access-token` |
| Headers | Partner Auth headers + `X-Ur-Id`      |

Request body: empty (`{}`).

UR reads `urId` from the `X-Ur-Id` header (which you already sign as part of Partner Auth on every `/api/fma/v1/*` request) and resolves the active onboarding session server-side. No `tokenId` or `network` fields are needed.

Response:

```json
{
  "code": 0,
  "message": "ok",
  "data": {
    "token": "_act-jwt-eyJhbGciOiJub25lIn0...."
  }
}
```

Response fields:

| Field   | Description                                                                                                |
| ------- | ---------------------------------------------------------------------------------------------------------- |
| `token` | Sumsub access token. TTL is 20 minutes. Pass this string to the Sumsub SDK constructor on the user device. |

Server-side validation rules:

* `X-Ur-Id` must be present (else `INVALID_PARAM`).
* The user must have an active onboarding session for `(partnerId, urId)` (else `NO_ACTIVE_SESSION`).
* The session's data channel must be the SDK path (else `SESSION_WRONG_CHANNEL`; check your partner config).

### 4.3 Get current session

Read the current onboarding state. Useful for polling after the user starts the Sumsub SDK.

| Item    | Value                             |
| ------- | --------------------------------- |
| Method  | `GET`                             |
| Path    | `/api/fma/v1/kyc/session/current` |
| Headers | Partner Auth headers + `X-Ur-Id`  |

Request: no query parameters or body.

Response with an active session:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "sessionId": "223d567f-ea11-4629-ba2a-94664edc26f6",
    "flowId": 11188,
    "retryLevel": 0,
    "state": "SignFormA",
    "dataChannel": "sdk",
    "createdAt": 1781000449,
    "lastUserActivityAt": 1781000789
  }
}
```

Response fields:

| Field                             | Description                                                                                                                                                                                                                                                                                                  |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `state`                           | Current session state. See the state enum table below.                                                                                                                                                                                                                                                       |
| `dataChannel`                     | Always `sdk` for this onboarding path. If you see anything else, your partner config is wrong.                                                                                                                                                                                                               |
| `retryLevel`                      | Counts the user's sessions: it increments each time a fresh session starts after a previous one terminated, including a retry session. This is **not** the `retryLevel` carried by the `fma.additional_kyc.*` webhooks, which names a retry category. See [Retry KYC](/api-reference/kyc-and-kyb/retry-kyc). |
| `createdAt`, `lastUserActivityAt` | Unix timestamps in seconds.                                                                                                                                                                                                                                                                                  |

A user with no active session receives `NO_ACTIVE_SESSION` (`code: 30031`); treat that as the post-onboarding steady state.

#### State enum

`state` reflects the step the session is currently parked on, in onboarding order:

| `state`                                | Phase           | Meaning                                                                                                                                                                                                                                   |
| -------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ConfirmYourCountryOfResidence`        | Sumsub SDK      | Initial state right after `/create-account`. The user is at the start of the Sumsub questionnaire, confirming their country of residence.                                                                                                 |
| `AddressProof`                         | Sumsub SDK      | The user is confirming and proving their residential address.                                                                                                                                                                             |
| `UnderstandRisk`                       | Sumsub SDK      | The user is completing the risk-understanding questionnaire.                                                                                                                                                                              |
| `LiabilityWaiver`                      | Sumsub SDK      | The user is acknowledging the liability waiver.                                                                                                                                                                                           |
| `IdOrPassportOrOtherIdInformationScan` | Sumsub SDK      | The user is scanning their ID card or passport.                                                                                                                                                                                           |
| `IdAndLiveness`                        | Sumsub SDK      | The user is completing the liveness check and face match.                                                                                                                                                                                 |
| `SignFormA`                            | Partner backend | The Sumsub phase finished with `GREEN`. Your backend should now call `/kyc/form-a-info`, collect the user's consent, `/kyc/sign-form`, then `/kyc/submit`. The state stays `SignFormA` through sign-form; only `/kyc/submit` advances it. |
| `Register`                             | UR backend      | `/kyc/submit` succeeded and UR's background activation scheduler owns the session. Nothing for you to do; wait for the `fma.account.result` webhook.                                                                                      |

Notes on reading `state`:

* Progress through the Sumsub-phase states is driven by Sumsub's webhooks to UR, so this endpoint can lag the SDK UI by a few seconds, and a poll loop will not necessarily observe every intermediate state.
* `Submitting`, `Completed`, and `Failed` never appear on this endpoint. `/kyc/submit` returns `Submitting` in its own response, but `/kyc/session/current` keeps reporting `Register` while activation is in flight. Once the session reaches a terminal outcome (activated or rejected), this endpoint returns `NO_ACTIVE_SESSION` (`code: 30031`); the outcome itself is delivered by the `fma.account.result` webhook (section 5).
* The order above describes an onboarding session, which always starts at `ConfirmYourCountryOfResidence`. A retry session runs a reduced step list and therefore starts partway down the table, for example directly at `IdOrPassportOrOtherIdInformationScan`. Do not treat the first observed state as evidence that the user is at the beginning of a flow, and do not fail on a state you did not expect. See [Retry KYC](/api-reference/kyc-and-kyb/retry-kyc).
* Treat any unlisted value as an in-progress state: keep the user in the Sumsub SDK and keep polling.

### 4.4 Get Form A

Render Form A from the verified KYC snapshot.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `GET`                            |
| Path    | `/api/fma/v1/kyc/form-a-info`    |
| Headers | Partner Auth headers + `X-Ur-Id` |
| Query   | `sessionId`                      |

Callable only while `state` is `SignFormA`.

Response:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "formAVersion": "v1",
    "text": "HANG ZHOU SHI, 09 June 2026\nMy name is XIAOLING KANG, born on 1990-09-10, ...",
    "textHash": "0x5aa556c0a1108e13eaed59014ec80d9c5a14dd659223aa8f3ac2291ca2e6c5bf"
  }
}
```

Response fields:

| Field          | Description                                                                                                                           |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `formAVersion` | Current Form A template version. Pin this in your audit trail.                                                                        |
| `text`         | The Markdown text the user must consent to. Render exactly as returned.                                                               |
| `textHash`     | Keccak-256 hash of the text bytes. Pass it back unchanged in `/kyc/sign-form` so UR can detect any tampering between render and sign. |

### 4.5 Sign Form A

UR's TurnKey wallet signs Form A on the user's behalf. The user authorized the text by consenting in your UI; you prove that authorization by echoing the `textHash`.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/kyc/sign-form`      |
| Headers | Partner Auth headers + `X-Ur-Id` |

Request body:

```json
{
  "sessionId": "223d567f-ea11-4629-ba2a-94664edc26f6",
  "textHash": "0x5aa556c0a1108e13eaed59014ec80d9c5a14dd659223aa8f3ac2291ca2e6c5bf"
}
```

Response:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "state": "SignFormA",
    "signature": "0x73bdf33acf3cc17029687da6880694491900ed59a3f9832a355a05d3dacd7fe53cd001defcbc3ddedb5b697b481b892d0e0a448aa6b97aa606ed2fa26bdb7d5a1c",
    "signerAddress": "0xeEdCEC0bCa761c1ecC933E7a01ea34EaA543132a"
  }
}
```

The `signerAddress` matches the `evmAddress` from `/create-account`. The returned `state` is still `SignFormA`; the next call (`/kyc/submit`) flips the session to `Submitting`.

Common errors:

| Code  | Constant              | Meaning                                                          | Recovery                                                                    |
| ----- | --------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------- |
| 20005 | `FORMA_TEXT_MISMATCH` | The stored text changed between `/form-a-info` and `/sign-form`. | Re-call `/form-a-info`, show the new text, then sign with the new hash.     |
| 20007 | `FORMA_INCOMPLETE`    | The session is missing required fields.                          | Should not happen after a Sumsub `GREEN`; contact your integration channel. |
| 40005 | `TURNKEY_SIGN_FAILED` | UR's signing subsystem hit a transient failure.                  | Retry after a short backoff.                                                |

### 4.6 Submit

Hand the session off to UR's background activation scheduler.

| Item    | Value                            |
| ------- | -------------------------------- |
| Method  | `POST`                           |
| Path    | `/api/fma/v1/kyc/submit`         |
| Headers | Partner Auth headers + `X-Ur-Id` |

Request body:

```json
{ "sessionId": "223d567f-ea11-4629-ba2a-94664edc26f6" }
```

Response:

```json
{
  "code": 0,
  "message": "",
  "data": {
    "state": "Submitting",
    "queued": false,
    "awaitingPenny": false
  }
}
```

`queued: false` is the success signal for this onboarding path; UR's background scheduler picks the session up immediately. Wait for the `fma.account.result` webhook to confirm `activated`.

## 5. Webhook contract

UR delivers a single event type during onboarding: `fma.account.result`. Two further events, `fma.additional_kyc.required` and `fma.additional_kyc.completed`, cover operations-initiated retries after the user is live; subscribe to those as well and see [Retry KYC](/api-reference/kyc-and-kyb/retry-kyc). Subscribe at your registered webhook URL and verify the request signature before processing. Webhooks are **EIP-191 signed** (not HMAC): `X-Api-Signature` carries V1 = `sign(body)` and `X-Api-Signature-V2` carries V2 = `sign(timestamp + "." + requestId + "." + body)`. See [Webhooks](https://docs.ur.app/developer-resources/webhook) and [Signature and verification](/api-reference/signature-and-verify) for the exact recipe.

### 5.1 Envelope

The body envelope is `{ event, data, timestamp }`. Delivery metadata (including the idempotency id) travels in `X-Webhook-*` headers, not in the body.

```http
POST https://your-webhook.example.com/ur/webhooks HTTP/1.1
Content-Type: application/json
X-Webhook-Event-Type: fma.account.result
X-Webhook-Request-Id: 223d567f-ea11-4629-ba2a-94664edc26f6
X-Webhook-Timestamp: 1781000945
X-Webhook-Attempt: 1
X-Api-Signature: 0x...
X-Api-Signature-V2: 0x...

{
  "event": "fma.account.result",
  "timestamp": 1781000945,
  "data": { }
}
```

**Dedupe on the `X-Webhook-Request-Id` header**; it is fixed for the lifetime of a message and repeats on every retry. UR retries non-2xx responses with the same request id (exponential backoff); your handler must be idempotent on that value. (`businessKey` is an internal UR dedup key and is not part of the wire envelope.)

### 5.2 Activated payload

```json
{
  "urId": 5643568810,
  "sessionId": "223d567f-ea11-4629-ba2a-94664edc26f6",
  "partnerId": "8209",
  "status": "activated",
  "occurredAt": 1781000945
}
```

### 5.3 Rejected payload

```json
{
  "urId": 5643568810,
  "sessionId": "223d567f-ea11-4629-ba2a-94664edc26f6",
  "partnerId": "8209",
  "status": "rejected",
  "occurredAt": 1781000945,
  "rejectCode": "SUMSUB_REJECTED",
  "rejectReason": "Sumsub workflow completed with RED at level Passport or National ID scan V5"
}
```

The most common `rejectCode` values for this onboarding path:

| rejectCode                                        | Source          | Meaning                                                      | Recommended user-facing action                                                                             |
| ------------------------------------------------- | --------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `SUMSUB_REJECTED`                                 | Sumsub          | Vendor-side terminal rejection.                              | Allow retry. Suggest clearer document images.                                                              |
| `KYC_REJECTED`                                    | Banking partner | Downstream compliance rejected the user after Sumsub passed. | Surface a generic non-onboarded message; do not encourage immediate retry.                                 |
| `SESSION_EXPIRED`                                 | UR maintenance  | The user abandoned mid-flow for more than seven days.        | Allow restart.                                                                                             |
| `NATIONALITY_RESTRICTED`, `RESIDENCY_UNSUPPORTED` | UR gates        | Country gates failed late.                                   | Should normally surface at `/create-account`; if it arrives via webhook, contact your integration channel. |

## 6. Failure handling

### 6.1 Sumsub rejection

When Sumsub returns `RED`, UR closes the onboarding session and dispatches `fma.account.result` with `status: rejected, rejectCode: SUMSUB_REJECTED`. Your platform can offer a retry by calling `/create-account` again with the same `X-External-User-Id`; UR mints a fresh `sessionId` and increments the internal `retryLevel`. The user's URID is preserved.

The retry is safe to attempt immediately. UR's onboarding pipeline keeps an identity reservation so the same Sumsub-verified person cannot have two active onboarding attempts at once; that reservation is released automatically when the current attempt terminates, including on Sumsub rejection.

### 6.2 User abandons mid-flow

If the user starts onboarding but never finishes, UR's background sweep marks the session expired after seven days of inactivity and dispatches `fma.account.result` with `status: rejected, rejectCode: SESSION_EXPIRED`. Your platform can offer a restart any time; calling `/create-account` after the expiration mints a fresh session.

While the seven-day window is open, `/kyc/session/current` continues to return the in-progress state. Your UI can resume the user from whichever step the session is in.

### 6.3 Identity reuse

UR enforces an anti-fraud check so that the same identity (matching on name plus date of birth) cannot run two simultaneous onboarding attempts across any partner. If the second attempt collides with an already-active first attempt, UR rejects the collision. Sequential retries by the same identity are always allowed; the previous attempt's reservation is released when it terminates.

You do not need to implement client-side dedup. UR handles the check; partners only see a rejection if the collision is intentional fraud.

### 6.4 Token refresh

Sumsub access tokens expire 20 minutes after issuance. If the user pauses partway through the SDK, your backend re-issues a token via `POST /api/fma/v1/kyc/sumsub-access-token`; the Sumsub SDK exposes a callback (typically called `getNewAccessToken`) for in-session refresh. UR re-binds the new token to the same Sumsub applicant, so the SDK resumes where the user left off.

## 7. Reference: integration test

The Go integration tests below ship in the UR backend repository and run against the testnet environment. Use them as a port reference for your language. Full source: `tools/callurbankapi/fma_sdk_test.go` and `tools/callurbankapi/fma_kyc_onboarding_test.go`.

### 7.1 Phase 1 and 2: account plus Sumsub token

```go
// TestFMASdkAccessToken_HappyPath validates the synchronous chain:
//   /create-account -> /kyc/session/current -> /api/fma/v1/kyc/sumsub-access-token
//
// The Sumsub workflow itself runs on the user device and is out of scope here.
func TestFMASdkAccessToken_HappyPath(t *testing.T) {
    cfg := loadFMASdkCfg(t)
    suffix := uniqSuffix()
    externalUserId := "sdk-token-happy-" + suffix

    // Step 1: create the account.
    createBody := fmatypes.CreateAccountReq{
        Email:          "sdk-happy+" + suffix + "@example.com",
        Nationality:    "CHN",
        Residency:      "CHN",
        Dob:            "1994-01-06",
        DocumentExpiry: "2036-05-05",
    }
    raw, status, err := callFMAValidatePOST(t, cfg, "api/fma/v1/create-account",
        createBody, "", externalUserId)
    require.NoError(t, err)
    require.Equal(t, 200, status)

    var caOK fmatypes.CreateAccountResult
    code, msg := decodeFMAEnvelope(t, raw, &caOK)
    require.Zerof(t, code, "/create-account should succeed: code=%d msg=%s", code, msg)
    urIdStr := strconv.FormatInt(caOK.UrId, 10)

    // Step 2: confirm the data channel and state.
    raw, _, err = callFMAValidateGET(t, cfg, "api/fma/v1/kyc/session/current",
        url.Values{}, urIdStr, externalUserId)
    require.NoError(t, err)
    var cur sdkSessionCurrentResult
    curCode, curMsg := decodeFMAEnvelope(t, raw, &cur)
    require.Zerof(t, curCode, "/kyc/session/current should succeed: code=%d msg=%s", curCode, curMsg)
    assert.Equal(t, "sdk", cur.DataChannel)
    assert.Equal(t, "ConfirmYourCountryOfResidence", cur.State)

    // Step 3: request the Sumsub token.
    // Empty body; urId is read from X-Ur-Id (FMAValidate middleware), and
    // UR resolves the active session server-side.
    raw, status, err = callFMAValidatePOST(t, cfg, "api/fma/v1/kyc/sumsub-access-token",
        struct{}{}, urIdStr, externalUserId)
    require.NoError(t, err)
    require.Equal(t, 200, status)

    var tokenResp struct {
        Token string `json:"token"`
    }
    code, msg = decodeFMAEnvelope(t, raw, &tokenResp)
    require.Zerof(t, code, "access-token should succeed: code=%d msg=%s", code, msg)
    assert.NotEmpty(t, tokenResp.Token)
}
```

### 7.2 Phase 3: sign and submit

```go
// TestFMASdkSignFormAndSubmit covers the post-Sumsub chain. Sumsub must have
// already returned GREEN, so the session is at state SignFormA.
//
// Run after the user has completed Sumsub on a device, with:
//   FMA_SDK_SESSION_ID=<uuid> FMA_SDK_UR_ID=<int> FMA_SDK_EXTERNAL_USER_ID=<str> \
//     go test -v -run TestFMASdkSignFormAndSubmit ./tools/callurbankapi
func TestFMASdkSignFormAndSubmit(t *testing.T) {
    sessionId := strings.TrimSpace(os.Getenv("FMA_SDK_SESSION_ID"))
    urIdStr := strings.TrimSpace(os.Getenv("FMA_SDK_UR_ID"))
    externalUserId := strings.TrimSpace(os.Getenv("FMA_SDK_EXTERNAL_USER_ID"))
    if sessionId == "" || urIdStr == "" || externalUserId == "" {
        t.Skip("set FMA_SDK_SESSION_ID + FMA_SDK_UR_ID + FMA_SDK_EXTERNAL_USER_ID env vars")
    }
    cfg := loadFMASdkCfg(t)

    // 4a: confirm state is SignFormA.
    raw, _, err := callFMAValidateGET(t, cfg, "api/fma/v1/kyc/session/current",
        url.Values{}, urIdStr, externalUserId)
    require.NoError(t, err)
    var cur sdkSessionCurrentResult
    code, msg := decodeFMAEnvelope(t, raw, &cur)
    require.Zerof(t, code, "/kyc/session/current should succeed: code=%d msg=%s", code, msg)
    require.Equal(t, "SignFormA", cur.State)

    // 4b: get Form A textHash.
    qs := url.Values{}
    qs.Set("sessionId", sessionId)
    raw, _, err = callFMAValidateGET(t, cfg, "api/fma/v1/kyc/form-a-info",
        qs, urIdStr, externalUserId)
    require.NoError(t, err)
    var formA struct {
        FormAVersion string `json:"formAVersion"`
        Text         string `json:"text"`
        TextHash     string `json:"textHash"`
    }
    code, msg = decodeFMAEnvelope(t, raw, &formA)
    require.Zerof(t, code, "/kyc/form-a-info should succeed: code=%d msg=%s", code, msg)
    require.NotEmpty(t, formA.TextHash)

    // 4c: TurnKey-sign Form A.
    signBody := fmatypes.SignFormReq{SessionId: sessionId, TextHash: formA.TextHash}
    raw, _, err = callFMAValidatePOST(t, cfg, "api/fma/v1/kyc/sign-form",
        signBody, urIdStr, externalUserId)
    require.NoError(t, err)
    var signOK fmatypes.SignFormResult
    code, msg = decodeFMAEnvelope(t, raw, &signOK)
    require.Zerof(t, code, "/kyc/sign-form should succeed: code=%d msg=%s", code, msg)
    require.NotEmpty(t, signOK.Signature)
    require.NotEmpty(t, signOK.SignerAddress)

    // 5: hand off to the background scheduler.
    submitBody := fmatypes.SubmitReq{SessionId: sessionId}
    raw, _, err = callFMAValidatePOST(t, cfg, "api/fma/v1/kyc/submit",
        submitBody, urIdStr, externalUserId)
    require.NoError(t, err)
    var submitOK fmatypes.SubmitResult
    code, msg = decodeFMAEnvelope(t, raw, &submitOK)
    require.Zerof(t, code, "/kyc/submit should succeed: code=%d msg=%s", code, msg)
    assert.Equal(t, "Submitting", submitOK.State)
    assert.False(t, submitOK.Queued)
    assert.False(t, submitOK.AwaitingPenny)
}
```

### 7.3 Partner Auth signing helper

```go
// EIP-191 personal_sign over canonicalMessage + " " + deadline.
// See section §4 above for canonical-message construction rules.
func fmaSignHeaders(t *testing.T, cfg fmaCfg, canonicalMessage string, deadlineSeconds int64) map[string]string {
    if deadlineSeconds <= 0 {
        deadlineSeconds = 60
    }
    deadline := time.Now().Unix() + deadlineSeconds
    deadlineStr := strconv.FormatInt(deadline, 10)
    signedMessage := canonicalMessage + " " + deadlineStr

    pk, _ := crypto.HexToECDSA(strings.TrimPrefix(cfg.privateKey, "0x"))

    // EIP-191 personal_sign prefix.
    prefix := fmt.Sprintf("\x19Ethereum Signed Message:\n%d", len(signedMessage))
    hash := crypto.Keccak256Hash([]byte(prefix + signedMessage))

    sig, _ := crypto.Sign(hash.Bytes(), pk)
    return map[string]string{
        "X-Api-Signature": "0x" + hex.EncodeToString(sig),
        "X-Api-Deadline":  deadlineStr,
        "X-Api-PublicKey": cfg.pubKey,
        "Content-Type":    "application/json",
    }
}

// Canonical for FMA endpoints: {body}urId:{X-Ur-Id}externalUserId:{X-External-User-Id}
func fmaValidateSignHeaders(t *testing.T, cfg fmaCfg, body, urId, externalUserId string,
    deadlineSeconds int64) map[string]string {
    canonical := body +
        fmt.Sprintf("urId:%s", urId) +
        fmt.Sprintf("externalUserId:%s", externalUserId)
    headers := fmaSignHeaders(t, cfg, canonical, deadlineSeconds)
    if urId != "" {
        headers["X-Ur-Id"] = urId
    }
    if externalUserId != "" {
        headers["X-External-User-Id"] = externalUserId
    }
    return headers
}
```

## 8. Error codes

The table below covers the codes that surface during this onboarding path. Endpoint-specific error tables for the rest of the Managed Custody Mode surface live next to each endpoint in [API reference: Managed Custody Mode](/api-reference/account/managed-custody-mode).

| Code           | Constant                | Where it surfaces                                             | Meaning                                                                                          |
| -------------- | ----------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| 10002          | `PARTNER_NOT_ALLOWED`   | `/create-account`                                             | `partnerId` is not on UR's allowlist; contact your integration channel.                          |
| 10004          | `PARTNER_MODE_MISMATCH` | `/create-account`                                             | The partner is not configured for the SDK onboarding path.                                       |
| 30010 to 30013 | country gates           | `/create-account`                                             | Sent `nationality`, `residency`, `dob`, or `documentExpiry` fails a country or age gate.         |
| 30015          | `L1_CONFLICT`           | `/create-account`                                             | The email is already onboarded by another partner.                                               |
| 30031          | `NO_ACTIVE_SESSION`     | `/api/fma/v1/kyc/sumsub-access-token`, `/kyc/session/current` | The user has no active onboarding session. Call `/create-account` first.                         |
| 30032          | `SESSION_WRONG_CHANNEL` | `/api/fma/v1/kyc/sumsub-access-token`                         | The active session is on a different onboarding path; partner config issue.                      |
| 20005          | `FORMA_TEXT_MISMATCH`   | `/kyc/sign-form`                                              | `textHash` does not match the current render; re-call `/kyc/form-a-info`.                        |
| 40005          | `TURNKEY_SIGN_FAILED`   | `/kyc/sign-form`                                              | UR's signing subsystem hit a transient failure; retry.                                           |
| 40003          | `UPSTREAM_UNAVAILABLE`  | any                                                           | UR's upstream (banking partner, TurnKey, Sumsub) is temporarily unavailable. Retry with backoff. |
| 50002          | `INTERNAL_ERROR`        | any                                                           | Surface a generic message and contact your integration channel.                                  |


# Shared-token KYC reuse

Onboard users whose KYC your platform already completed in its own Sumsub tenant, by sharing the verified applicant with UR through a Sumsub share token. UR copies the applicant, validates the data, a

This page documents the **shared-token** onboarding path: your platform runs a clone of UR's KYC workflow in **your own** Sumsub tenant, and once a user is approved you share that applicant with UR using a Sumsub **share token**. UR imports the applicant into UR's Sumsub tenant, validates the shared data, and takes the user to a `Live` UR Account.

Use this path when you already operate a Sumsub tenant and want to avoid making the user redo KYC. If you do **not** run a Sumsub tenant, use [Managed Custody SDK KYC](/api-reference/kyc-and-kyb/managed-custody-sdk-kyc) instead (UR hosts the Sumsub relationship and the SDK runs KYC on the user's device).

{% hint style="info" %}
Shared-token reuse requires your `partnerId` to be provisioned for `shared-token` mode and paired with UR's Sumsub tenant. Coordinate with your dedicated integration channel before pointing production traffic at it. See [§8 Prerequisites](#8-prerequisites).
{% endhint %}

{% hint style="warning" %}
This supersedes the share-token section of [API reference: Managed Custody Mode](/api-reference/account/managed-custody-mode) §3. The current implementation uses a **dedicated handoff endpoint** (`/kyc/reuse-share-token`), not an `applicantId` on `/create-account`, and UR **does not re-run** the KYC checks; see [§1](#1-how-it-works).
{% endhint %}

## 1. How it works

The compliance-grade KYC (including NFC document authentication) happens in **your** Sumsub tenant, running a workflow that UR clones from its own KYC level definitions during onboarding. That is the point at which the regulatory checks are performed.

When you hand the approved applicant to UR:

1. Your backend mints a **single-use share token** scoped to UR's Sumsub `clientId`, valid for a short TTL (about 20 minutes).
2. Your backend calls UR's **handoff endpoint** with that token.
3. UR **copies** the applicant into UR's own Sumsub tenant (Sumsub "Copy Applicant") and performs **data-level validation** on the copied profile: completeness (all required fields present), document images and NFC data present, and eligibility (nationality / residency / age / document expiry). UR **does not re-run** AML/PEP or re-verify the documents; your cloned workflow already did that.
4. UR returns a conclusive verdict for the attempt: `passed`, `incomplete` (remediable; the user must supply what's missing), or a non-remediable terminal rejection.
5. On `passed`, the user signs Form A and your backend submits; UR provisions the account and activates it. On `incomplete`, the user completes the missing level in **your** Sumsub workflow and you hand off again.

Shared-token reuse is available for both account models:

|                  | Managed Custody (this page's primary flow)                                   | External Wallet Access                                  |
| ---------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- |
| Signing          | Partner Auth (EIP-191), identity in `X-Ur-Id` header                         | Partner Auth (EIP-191), `urId` in the request body      |
| Handoff endpoint | `POST /api/fma/v1/kyc/reuse-share-token`                                     | `POST /api/v1/sumsub/reuse-share-token`                 |
| Poll / submit    | `/api/fma/v1/kyc/check`, `/kyc/form-a-info`, `/kyc/sign-form`, `/kyc/submit` | `/api/v1/sumsub/kyc-check`, `/api/v1/sumsub/kyc-submit` |
| Form A signer    | UR-managed custodial wallet                                                  | The user's own wallet                                   |

The rest of this page describes the **Managed Custody** flow. See [§7 External Wallet variant](#7-external-wallet-variant) for the differences.

## 2. End-to-end sequence

```mermaid
sequenceDiagram
    autonumber
    participant U as End user
    participant PB as Partner backend
    participant PS as Partner Sumsub tenant
    participant UR as UR OpenAPI
    participant US as UR Sumsub tenant

    Note over U,PS: User completes KYC in your cloned UR workflow
    PS-->>PB: applicantReviewed = GREEN
    PB->>UR: POST /create-account (email, X-External-User-Id)
    UR-->>PB: urId, sessionId, evmAddress, state=PartnerDataIngestion
    PB->>PS: Generate share token (forClientId = UR clientId, single-use)
    PS-->>PB: shareToken
    PB->>UR: POST /kyc/reuse-share-token (shareToken, X-Ur-Id)
    UR->>US: Preview + Copy Applicant + data validation
    US-->>UR: imported applicant
    alt verdict = passed
        UR-->>PB: { status: passed }
        UR-->>PB: webhook fma.kyc.reuse_check.result (passed)
        PB->>UR: GET /kyc/form-a-info → text + textHash
        PB->>UR: POST /kyc/sign-form (textHash)
        PB->>UR: POST /kyc/submit
        UR-->>PB: webhook fma.account.result (activated)
    else verdict = incomplete
        UR-->>PB: { status: incomplete, missingFields }
        UR-->>PB: webhook fma.kyc.reuse_check.result (incomplete, requiredLevels)
        Note over U,PS: User completes the missing level in your workflow
        PB->>UR: POST /kyc/reuse-share-token (new shareToken)  %% same session, next attempt
    end
```

## 3. Integration walkthrough

### 3.1 Complete KYC in your Sumsub workflow

Run the user through the UR-cloned workflow in your Sumsub tenant. Present the required **data-sharing declaration** before identity verification and record the user's agreement; this is the user-facing legal basis for sharing the applicant with UR (see [the required KYC disclosure](https://docs.ur.app/getting-started/integration-guide#kyc-data-sharing-disclosure)). Only hand off applicants whose review answer is **GREEN**.

### 3.2 Create the UR Account

Call `POST /api/fma/v1/create-account` with `X-External-User-Id` and the user's email. UR mints (or reuses) the URID, provisions the UR-managed wallet, and returns `sessionId`, `urId`, `evmAddress`, and `state = PartnerDataIngestion`. You do **not** pass a Sumsub `applicantId` here; the applicant is delivered later through the handoff endpoint.

### 3.3 Mint a share token

After the applicant is GREEN, mint a **single-use** Sumsub share token scoped to UR's `clientId`. Mint a fresh token for every handoff attempt; Sumsub invalidates a token once it is used.

### 3.4 Hand off the applicant

Call `POST /api/fma/v1/kyc/reuse-share-token` with the `shareToken` in the body and `X-Ur-Id` set to the returned `urId`. UR previews the token (does not consume it), copies the applicant, validates the data, and returns a conclusive verdict synchronously:

* `status = passed`: proceed to Form A.
* `status = incomplete` with `missingFields`: the user must complete the missing data in your workflow, then hand off again with a **new** token.
* A terminal rejection (e.g. `AGE_UNDER_18`, `NATIONALITY_RESTRICTED`): the session cannot proceed; this is non-remediable.

You may either read this synchronous response or consume the `fma.kyc.reuse_check.result` webhook (see [§6.1](#6-1-fma-kyc-reuse-check-result)); both carry the same verdict.

### 3.5 Poll completeness (optional)

`POST /api/fma/v1/kyc/check` with `sessionId` returns the last handoff attempt's verdict as a pure read (`complete`, `state`, `missingFields`). Use it as a fallback to the webhook. Poll every 30 seconds to 2 minutes; stop after `/kyc/submit` succeeds.

### 3.6 Render Form A and capture consent

`GET /api/fma/v1/kyc/form-a-info?sessionId=…` returns the exact Form A `text`, its `formAVersion`, and a `textHash`. Display the text to the user and capture consent. If `missingFields` is non-empty, do not proceed.

### 3.7 Sign Form A

`POST /api/fma/v1/kyc/sign-form` with `sessionId` and the `textHash` from the previous step. UR signs Form A with the user's UR-managed custodial wallet and advances the session toward `Register`.

### 3.8 Submit

`POST /api/fma/v1/kyc/submit` with `sessionId`. UR runs the final register against the banking core asynchronously. When the account activates, UR sends the `fma.account.result` webhook with `status = activated`, and the UR Account reaches `Live`.

## 4. API reference

All endpoints use Partner Auth (EIP-191) as described in [Managed Custody Mode §2.2](/api-reference/account/managed-custody-mode#2-2-authentication-partner-auth-eip-191). User-scoped endpoints identify the user with `X-Ur-Id` (or `X-External-User-Id`).

### 4.1 Create account

`POST /api/fma/v1/create-account`

| Field            | Required | Description       |
| ---------------- | -------- | ----------------- |
| `email`          | Yes      | User email.       |
| `nationality`    | No       | ISO-3166 alpha-3. |
| `residency`      | No       | ISO-3166 alpha-3. |
| `dob`            | No       | `YYYY-MM-DD`.     |
| `documentExpiry` | No       | `YYYY-MM-DD`.     |

Only `email` (plus the `X-External-User-Id` header) is required at this endpoint. `nationality` / `residency` / `dob` / `documentExpiry` are optional here; partners may submit them later via `/kyc/sync-data`, and the eligibility gates are deferred to `/kyc/check`, which re-validates over the merged staging snapshot before `/kyc/submit`.

Identity: send `X-External-User-Id`. Response `data`: `sessionId`, `urId`, `evmAddress`, `state` (`PartnerDataIngestion` for shared-token).

### 4.2 Hand off share token

`POST /api/fma/v1/kyc/reuse-share-token` · identity: `X-Ur-Id`

| Field        | Required | Description                                                   |
| ------------ | -------- | ------------------------------------------------------------- |
| `shareToken` | Yes      | Single-use Sumsub share token, `forClientId` = UR's clientId. |

Response `data`:

| Field           | Description                                                     |
| --------------- | --------------------------------------------------------------- |
| `status`        | `passed`, `incomplete`, or `terminal`.                          |
| `applicantId`   | The imported applicant ID in UR's tenant (present once copied). |
| `attempt`       | 1-indexed handoff attempt within this session.                  |
| `missingFields` | Present on `incomplete`; machine paths of the gaps.             |

The top-level envelope `code` is the authoritative outcome; `data.status` mirrors it: `passed` → `code = 0`; `incomplete` → `code = 20004`; `terminal` (non-remediable) → a numeric eligibility code (`30010` / `30011` / `30012`, see [§9](#9-error-codes)). On both `incomplete` and `terminal` the envelope carries the non-zero business `code` **and** `data.status` set to the matching discriminator; branch on whichever your integration prefers.

### 4.3 Check

`POST /api/fma/v1/kyc/check` · identity: `X-Ur-Id` · body `{ "sessionId": "…" }`

Pure read of the last handoff verdict. Response `data`: `state`, `complete` (bool), `missingFields`.

### 4.4 Get Form A

`GET /api/fma/v1/kyc/form-a-info?sessionId=…` · identity: `X-Ur-Id`

Response `data`: `formAVersion`, `text`, `textHash` (echo `text`/`textHash` into sign-form). This endpoint returns only these three fields.

### 4.5 Sign Form A

`POST /api/fma/v1/kyc/sign-form` · identity: `X-Ur-Id` · body `{ "sessionId": "…", "textHash": "0x…" }`

Response `data`: `state`, `signature`, `signerAddress` (UR-managed wallet).

### 4.6 Submit

`POST /api/fma/v1/kyc/submit` · identity: `X-Ur-Id` · body `{ "sessionId": "…" }`

Response `data`: `state`, `queued`. Register proceeds asynchronously; wait for the `fma.account.result` webhook.

## 5. Verdict & session state model

One handoff call = **one attempt** on one share token. Remediable failures are attempts within a **living session**; only non-remediable verdicts end the session.

| Verdict                   | Session                                               | What the user does next                                                       |
| ------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------- |
| `passed`                  | Advances to `SignFormA`                               | Sign Form A, submit                                                           |
| `incomplete`              | Unchanged; attempt recorded                           | Complete the missing level in your workflow, mint a new token, hand off again |
| terminal (non-remediable) | Ends (subsequent handoffs return `NO_ACTIVE_SESSION`) | Nothing; the user is not eligible                                             |

The `incomplete` loop above is a retry **within one onboarding session**, driven by you. It is a different mechanism from the operations-initiated retry on [Retry KYC](/api-reference/kyc-and-kyb/retry-kyc), which applies to a user who is already onboarded and starts with `POST /api/fma/v1/kyc/session/create` after an `fma.additional_kyc.required` webhook.

## 6. Webhook contract

Webhooks are delivered per [Webhooks](https://docs.ur.app/developer-resources/webhook) (envelope `{event, data, timestamp}`, EIP-191 signed, `X-Webhook-Request-Id` idempotency). Subscribe your `partnerId` to both event types below. Two further events, `fma.additional_kyc.required` and `fma.additional_kyc.completed`, cover operations-initiated retries after the user is live; subscribe to those as well and see [Retry KYC](/api-reference/kyc-and-kyb/retry-kyc).

### 6.1 fma.kyc.reuse\_check.result

The async mirror of a handoff attempt's verdict. Sent for `passed` and `incomplete` (terminal verdicts are not webhooked in v1). Idempotency business key: `{urId}:kyc:reuse_check:{attempt}`.

```json
{
  "event": "fma.kyc.reuse_check.result",
  "data": {
    "urId": 6233490772,
    "partnerId": "8208",
    "sessionId": "1c1f3904-…",
    "attempt": 1,
    "status": "incomplete",
    "missingFields": ["nfc_data", "registerRequest.address.addressProof"],
    "requiredLevels": [
      { "levelName": "<your NFC level>", "action": "RERUN_LEVEL", "fields": ["nfc_data"] }
    ],
    "occurredAt": 1784218179,
    "correlationId": "c514d9ea-…"
  },
  "timestamp": 1784218179
}
```

On `status = passed`, `missingFields` / `requiredLevels` are omitted. Partners that only act on remediation may ignore `passed`.

### 6.2 fma.account.result

Sent when the account activates after submit. Payload:

```json
{
  "event": "fma.account.result",
  "data": {
    "urId": 6233490772,
    "status": "activated",
    "partnerId": "8208",
    "sessionId": "9abf50d4-…",
    "occurredAt": 1784217340
  },
  "timestamp": 1784217340
}
```

## 7. External Wallet variant

For External Wallet Access, the user holds their own wallet. The flow is the same, with these differences:

* **Auth / identity:** Partner Auth signs the raw body; the `urId` travels in the request body, not the `X-Ur-Id` header.
* **Handoff:** `POST /api/v1/sumsub/reuse-share-token` with `{ shareToken, urId }`. The first handoff lazily creates the onboarding session.
* **Poll:** `POST /api/v1/sumsub/kyc-check`.
* **Submit / Form A:** `POST /api/v1/sumsub/kyc-submit`; the user signs Form A with their own wallet (there is no custodial signer).

The `fma.kyc.reuse_check.result` and `fma.account.result` webhooks are identical.

## 8. Prerequisites

One-time setup, coordinated with your UR integration channel:

1. **Partner pairing.** Your platform and UR are configured as Donor / Recipient in Sumsub so your share tokens are accepted by UR's tenant.
2. **Cloned workflow.** UR shares its KYC level definitions and Sumsub clones UR's KYC workflow (GPS / questionnaire / passport + NFC / liveness) into your tenant. Run users through this workflow so the shared applicant carries every field UR validates.
3. **Provisioning.** Your `partnerId` is set to `fmaMode = shared-token` with a per-partner `sourceKey`, your signer address is registered, and your webhook subscriptions include `fma.kyc.reuse_check.result` and `fma.account.result`.
4. **Declaration.** Your KYC flow presents the required data-sharing declaration and records the user's agreement before identity verification.
5. **Sandbox.** Validate end to end against the testnet base URL before production.

## 9. Error codes

Business errors are returned with HTTP 200 and a non-zero `code` in the response envelope.

| Code    | Meaning                                                    | Action                                                                                                |
| ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `20003` | Invalid / missing parameter (e.g. `shareToken`, `X-Ur-Id`) | Fix the request.                                                                                      |
| `20004` | Session incomplete                                         | Read `missingFields`; remediate and hand off again.                                                   |
| `20011` | Share token invalid                                        | Mint a fresh single-use token (tokens invalidate on use).                                             |
| `30031` | No active KYC session                                      | Call `/create-account` first, or the session already ended (terminal).                                |
| `30032` | Session on the wrong channel                               | Confirm the partner is in `shared-token` mode.                                                        |
| `30008` | Identity already active in another session                 | The same identity is active in another session; resolve the existing one before reusing the identity. |
| `30010` | Nationality restricted (message: `NATIONALITY_RESTRICTED`) | Non-remediable eligibility rejection; the user is not eligible.                                       |
| `30011` | Residency unsupported (message: `RESIDENCY_UNSUPPORTED`)   | Non-remediable eligibility rejection; the user is not eligible.                                       |
| `30012` | Age under 18 (message: `AGE_UNDER_18`)                     | Non-remediable eligibility rejection; the user is not eligible.                                       |

> The eligibility codes are numeric in the envelope `code`; the string labels (`NATIONALITY_RESTRICTED` etc.) appear only in the human-readable `message`.


# Retry KYC

Handle a UR-initiated request for a user to redo part or all of their KYC. UR issues a retry directive, notifies you over webhook, and your platform claims a new session and drives the user through th

This page documents retry KYC (also called additional KYC): the flow UR uses when an already-onboarded user must resubmit some or all of their KYC data. Typical triggers are a compliance re-check on a live account, remediation after Fiat24 rejects an account, and an onboarding session that expired before it completed.

Retry KYC reuses the endpoints you already integrated for onboarding. What changes is the entry point: instead of calling `/create-account`, you claim a session that UR created for you, then run only the steps UR asks for.

{% hint style="info" %}
Only UR operations staff can start a retry. Neither your platform nor the user can start one. `POST /api/fma/v1/kyc/session/create` fails with `30034` when no retry directive is pending for the user.
{% endhint %}

{% hint style="warning" %}
**"Retry" means two different things across these pages.** They are separate mechanisms and do not share endpoints.

The onboarding restart path is documented on [Managed Custody SDK KYC](/api-reference/kyc-and-kyb/managed-custody-sdk-kyc) and [Shared-token KYC reuse](/api-reference/kyc-and-kyb/shared-token-kyc-reuse). Do not call `/create-account` to service a retry directive, and do not wait for a retry directive to let a rejected user try onboarding again.
{% endhint %}

|               | Retry KYC (this page)                                                      | Onboarding restart                                                          |
| ------------- | -------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| Who starts it | UR operations only                                                         | Your platform                                                               |
| When          | The user is already onboarded, and compliance wants data resubmitted       | Onboarding failed, for example Sumsub returned `RED`                        |
| How you enter | `POST /api/fma/v1/kyc/session/create`, after `fma.additional_kyc.required` | `POST /api/fma/v1/create-account` again, with the same `X-External-User-Id` |
| Scope         | Only the steps `taskType` names                                            | The whole onboarding flow                                                   |

## 1. Where this fits

Retry KYC works with the onboarding path your `partnerId` already uses. The `dataChannel` field in the webhook payload tells you which path applies to the user, and the steps you run come from that path:

| `dataChannel`  | Your onboarding path                                                          | Retry entry point for the user data                                         |
| -------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `push`         | Data push to UR                                                               | `POST /api/fma/v1/kyc/sync-data`                                            |
| `sdk`          | [Managed Custody SDK KYC](/api-reference/kyc-and-kyb/managed-custody-sdk-kyc) | `POST /api/fma/v1/kyc/sumsub-access-token`, then the Sumsub SDK             |
| `shared-token` | [Shared-token KYC reuse](/api-reference/kyc-and-kyb/shared-token-kyc-reuse)   | `POST /api/fma/v1/kyc/reuse-share-token`, then `POST /api/fma/v1/kyc/check` |

{% hint style="warning" %}
Partners on the pull integration (`dataChannel` returns `pull`) are not covered by the current retry catalogue. If you receive a retry webhook with `dataChannel: "pull"`, use your dedicated integration channel before you act on it.
{% endhint %}

Two payload fields drive everything you do:

* `taskType` tells you what the user must redo. Treat this field as authoritative.
* `fiat24Mode` tells you how the session finishes, and specifically whether you call `/kyc/submit`.

### What to read for your channel

Every section applies to every channel except the per-channel walkthroughs in section 5, which you can read selectively:

| Your `dataChannel` | Read in section 5    | Skip                                                                                                                         |
| ------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `push`             | Push channel         | SDK channel, shared-token channel, and the two SDK-only notes about the access-token timeout and the SDK closing immediately |
| `sdk`              | SDK channel          | Push channel, shared-token channel                                                                                           |
| `shared-token`     | Shared-token channel | Push channel, SDK channel                                                                                                    |

Read [Webhooks](https://docs.ur.app/developer-resources/webhook) alongside this page for the envelope, signature verification, and delivery contract that apply to both retry events.

## 2. Lifecycle

The diagram below traces one user end to end: first-time onboarding, registration with the banking partner, going live, and then a retry that UR operations issue later. Part A is onboarding, covered in depth on your channel's own page; part B is what this page documents.

Pick your channel. The retry phases are structurally identical across channels, and only the data collection step differs.

{% tabs %}
{% tab title="SDK channel" %}

```mermaid
sequenceDiagram
    autonumber
    participant U as End user
    participant PA as Partner app
    participant PB as Partner backend
    participant UR as UR OpenAPI
    participant SS as UR Sumsub tenant
    participant OPS as UR operations

    rect rgb(240,250,255)
    note over PB,SS: Part A1. First-time onboarding
    PB->>UR: POST /api/fma/v1/create-account
    UR-->>PB: 200 sessionId, urId, state ConfirmYourCountryOfResidence
    PB->>UR: POST /api/fma/v1/kyc/sumsub-access-token
    UR-->>PB: 200 token
    PB->>PA: Relay the token
    PA->>SS: Launch the Sumsub SDK
    U->>SS: Complete every level
    SS->>UR: applicantWorkflowCompleted GREEN
    note over UR: UR stores the applicant snapshot<br/>and advances state to SignFormA
    end

    rect rgb(245,255,245)
    note over PB,UR: Part A2. Form A, submit, register
    PB->>UR: GET /api/fma/v1/kyc/form-a-info
    PB->>U: Display Form A, obtain consent
    PB->>UR: POST /api/fma/v1/kyc/sign-form
    PB->>UR: POST /api/fma/v1/kyc/submit
    note over UR: UR registers the user with the banking partner<br/>and mints the URID to Live. This happens once per user.
    UR->>PB: Webhook fma.account.result status activated
    end

    rect rgb(255,250,235)
    note over OPS,PB: Part B1. Operations issue a retry
    OPS->>UR: Create retry directive
    note over UR: UR seals the user current session,<br/>so any sessionId you hold stops working
    UR->>PB: Webhook fma.additional_kyc.required
    note over PB: Branch on taskType and fiat24Mode
    end

    rect rgb(240,250,255)
    note over PB,UR: Part B2. Claim the retry session
    PB->>UR: POST /api/fma/v1/kyc/session/create
    UR-->>PB: 200 sessionId
    end

    rect rgb(245,255,245)
    note over PB,SS: Part B3. The user redoes only the targeted steps
    PB->>UR: POST /api/fma/v1/kyc/sumsub-access-token
    note over UR,SS: UR resets the targeted Sumsub steps and<br/>verifies each reset before issuing the token.<br/>Allow 60s or more on this call.
    UR-->>PB: 200 token
    PB->>PA: Relay the token
    PA->>SS: Launch the Sumsub SDK
    U->>SS: Redo the targeted step only
    SS->>UR: Verdict for the redone step
    end

    rect rgb(250,245,255)
    note over PB,UR: Part B4. Re-sign Form A and finish
    PB->>UR: GET /api/fma/v1/kyc/form-a-info
    PB->>UR: POST /api/fma/v1/kyc/sign-form
    alt fiat24Mode ops_offline
        note over UR: Already registered, so no submit.<br/>The session completes on its own.
    else fiat24Mode auto_register
        PB->>UR: POST /api/fma/v1/kyc/submit
        UR->>PB: Webhook fma.account.result status activated
    end
    UR->>PB: Webhook fma.additional_kyc.completed
    end
```

Part A is documented in full on [Managed Custody SDK KYC](/api-reference/kyc-and-kyb/managed-custody-sdk-kyc).
{% endtab %}

{% tab title="Push channel" %}

```mermaid
sequenceDiagram
    autonumber
    participant U as End user
    participant PA as Partner app
    participant PB as Partner backend
    participant UR as UR OpenAPI
    participant OPS as UR operations

    rect rgb(240,250,255)
    note over PB,UR: Part A1. First-time onboarding
    PB->>UR: POST /api/fma/v1/create-account
    UR-->>PB: 200 sessionId, urId, state PartnerDataIngestion
    U->>PA: Provide KYC data in your own UI
    PA-->>PB: Collected data
    PB->>UR: POST /api/fma/v1/kyc/sync-data
    PB->>UR: Identity verification, penny transfer or NFC read
    note over UR: state advances to SignFormA
    end

    rect rgb(245,255,245)
    note over PB,UR: Part A2. Form A, submit, register
    PB->>UR: GET /api/fma/v1/kyc/form-a-info
    PB->>U: Display Form A, obtain consent
    PB->>UR: POST /api/fma/v1/kyc/sign-form
    PB->>UR: POST /api/fma/v1/kyc/submit
    note over UR: UR registers the user with the banking partner<br/>and mints the URID to Live. This happens once per user.
    UR->>PB: Webhook fma.account.result status activated
    end

    rect rgb(255,250,235)
    note over OPS,PB: Part B1. Operations issue a retry
    OPS->>UR: Create retry directive
    note over UR: UR seals the user current session,<br/>so any sessionId you hold stops working
    UR->>PB: Webhook fma.additional_kyc.required
    note over PB: Branch on taskType.<br/>requiredFields names the exact paths to fix.
    end

    rect rgb(240,250,255)
    note over PB,UR: Part B2. Claim the retry session
    PB->>UR: POST /api/fma/v1/kyc/session/create
    UR-->>PB: 200 sessionId
    end

    rect rgb(245,255,245)
    note over PB,U: Part B3. You recollect and push the data
    PB->>PA: Ask the user for the data named by taskType
    PA->>U: Collect it in your own UI
    U-->>PB: Corrected data
    PB->>UR: POST /api/fma/v1/kyc/sync-data
    note over PB,UR: requiredFields non-empty: send only those paths.<br/>taskType full: send the complete payload,<br/>because UR starts the snapshot empty.
    opt taskType full only
        PB->>UR: Identity verification, penny transfer or NFC read
    end
    end

    rect rgb(250,245,255)
    note over PB,UR: Part B4. Re-sign Form A and finish
    PB->>UR: GET /api/fma/v1/kyc/form-a-info
    PB->>UR: POST /api/fma/v1/kyc/sign-form
    alt fiat24Mode ops_offline
        note over UR: Already registered, so no submit.<br/>The session completes on its own.
    else fiat24Mode auto_register
        PB->>UR: POST /api/fma/v1/kyc/submit
        UR->>PB: Webhook fma.account.result status activated
    end
    UR->>PB: Webhook fma.additional_kyc.completed
    end
```

No Sumsub SDK is involved on this channel. All data collection happens in your own UI, during onboarding and during a retry alike.
{% endtab %}

{% tab title="Shared-token channel" %}

```mermaid
sequenceDiagram
    autonumber
    participant U as End user
    participant PS as Partner Sumsub tenant
    participant PB as Partner backend
    participant UR as UR OpenAPI
    participant US as UR Sumsub tenant
    participant OPS as UR operations

    rect rgb(240,250,255)
    note over U,US: Part A1. First-time onboarding
    U->>PS: Complete KYC in your cloned UR workflow
    PS-->>PB: applicantReviewed GREEN
    PB->>UR: POST /api/fma/v1/create-account
    UR-->>PB: 200 sessionId, urId, state PartnerDataIngestion
    PB->>PS: Mint a single-use share token for UR clientId
    PB->>UR: POST /api/fma/v1/kyc/reuse-share-token
    UR->>US: Copy Applicant and validate
    UR->>PB: Webhook fma.kyc.reuse_check.result passed
    end

    rect rgb(245,255,245)
    note over PB,UR: Part A2. Form A, submit, register
    PB->>UR: GET /api/fma/v1/kyc/form-a-info
    PB->>U: Display Form A, obtain consent
    PB->>UR: POST /api/fma/v1/kyc/sign-form
    PB->>UR: POST /api/fma/v1/kyc/submit
    note over UR: UR registers the user with the banking partner<br/>and mints the URID to Live. This happens once per user.
    UR->>PB: Webhook fma.account.result status activated
    end

    rect rgb(255,250,235)
    note over OPS,PB: Part B1. Operations issue a retry
    OPS->>UR: Create retry directive
    note over UR: UR seals the user current session,<br/>so any sessionId you hold stops working
    UR->>PB: Webhook fma.additional_kyc.required
    note over PB: Branch on taskType and fiat24Mode
    end

    rect rgb(240,250,255)
    note over PB,UR: Part B2. Claim the retry session
    PB->>UR: POST /api/fma/v1/kyc/session/create
    UR-->>PB: 200 sessionId
    end

    rect rgb(245,255,245)
    note over U,US: Part B3. The user redoes the steps in your tenant
    U->>PS: Redo the steps named by taskType
    PB->>PS: Mint a fresh single-use share token
    note over PB: A token minted for the earlier session<br/>is not valid for the retry session
    PB->>UR: POST /api/fma/v1/kyc/reuse-share-token
    UR->>US: Copy Applicant and validate
    PB->>UR: POST /api/fma/v1/kyc/check
    UR-->>PB: 200 verdict
    end

    rect rgb(250,245,255)
    note over PB,UR: Part B4. Re-sign Form A and finish
    PB->>UR: GET /api/fma/v1/kyc/form-a-info
    PB->>UR: POST /api/fma/v1/kyc/sign-form
    alt fiat24Mode ops_offline
        note over UR: Already registered, so no submit.<br/>The session completes on its own.
    else fiat24Mode auto_register
        PB->>UR: POST /api/fma/v1/kyc/submit
        UR->>PB: Webhook fma.account.result status activated
    end
    UR->>PB: Webhook fma.additional_kyc.completed
    end
```

Part A is documented in full on [Shared-token KYC reuse](/api-reference/kyc-and-kyb/shared-token-kyc-reuse).
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Registration with the banking partner happens **once per user**, in part A2. That is why most retries carry `fiat24Mode: "ops_offline"` and need no `/kyc/submit`. You see `auto_register` on a retry only when the user never completed that registration, for example when their first onboarding session expired before it reached submit. In that case the retry finishes registration and you receive `fma.account.result` with `status: "activated"` in addition to `fma.additional_kyc.completed`.
{% endhint %}

## 3. Webhooks

Retry KYC uses two events. Both belong to the partner-managed (FMA) event family and are sent only to partners on an FMA integration. Subscribe to both:

| Event                          | UR sends it when                                     | You act by                                        |
| ------------------------------ | ---------------------------------------------------- | ------------------------------------------------- |
| `fma.additional_kyc.required`  | UR operations creates the directive                  | Claiming the session and guiding the user         |
| `fma.additional_kyc.completed` | The user finishes the last step of the retry session | Recording completion; no further call is required |

{% hint style="danger" %}
Subscribe to `fma.additional_kyc.required` **and** `fma.additional_kyc.completed`. UR drops an event that has no subscriber, and your platform receives nothing. Confirm both subscriptions through your dedicated integration channel before production traffic starts.
{% endhint %}

### fma.additional\_kyc.required

UR sends the following when the directive is created. Like every UR webhook, the retry events arrive in the standard `{event, data, timestamp}` envelope described in [Webhooks](https://docs.ur.app/developer-resources/webhook):

```json
{
  "event": "fma.additional_kyc.required",
  "timestamp": 1785392900,
  "data": {
    "directiveId": "62fb1d29-e584-448f-a770-9454c94dbe24",
    "type": "retry",
    "taskType": "passport",
    "retryLevel": 6,
    "fiat24Mode": "ops_offline",
    "dataChannel": "sdk",
    "partnerId": "8509",
    "externalUserId": "your-user-id",
    "urId": 5139803526,
    "retryOfSessionId": "a67efa78-da9b-4404-a77d-97f93b8085a9",
    "retryReason": "Compliance review: document expired",
    "requiredFields": [],
    "deadlineAt": 0,
    "createdAt": 1785392900
  }
}
```

The `data` fields carry the following meaning:

| Field              | Type   | Meaning                                                                                                                                                                                                                                                                                      |
| ------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `directiveId`      | string | Identifies this retry request. Use it to correlate the `completed` event and for support tickets.                                                                                                                                                                                            |
| `type`             | string | Always `retry` for this flow. Other values are reserved.                                                                                                                                                                                                                                     |
| `taskType`         | string | What the user must redo: `full`, `passport`, `address_recheck`, or `redo_form_a`. See section 5.                                                                                                                                                                                             |
| `retryLevel`       | number | The retry category UR operations selected. Informational; `taskType` is the field you act on.                                                                                                                                                                                                |
| `fiat24Mode`       | string | `auto_register` or `ops_offline`. Decides whether you call `/kyc/submit`. See section 6.                                                                                                                                                                                                     |
| `dataChannel`      | string | `push`, `sdk`, or `shared-token`. Matches your integration path.                                                                                                                                                                                                                             |
| `externalUserId`   | string | Your user identifier, the same value you send in `X-External-User-Id`.                                                                                                                                                                                                                       |
| `urId`             | number | The user URID, as a 64-bit integer.                                                                                                                                                                                                                                                          |
| `retryOfSessionId` | string | The session this retry replaces. That session is already sealed.                                                                                                                                                                                                                             |
| `retryReason`      | string | Free text from UR operations. Safe to show to your support staff, not to the user.                                                                                                                                                                                                           |
| `requiredFields`   | array  | Push channel only: the exact field paths to correct, for example `registerRequest.address.street`. Empty for other channels.                                                                                                                                                                 |
| `deadlineAt`       | number | Unix seconds, or `0` when no deadline is set. Informational only: UR does not expire the directive or the session when it passes, and no event fires at the deadline. Use it to prioritize your outreach to the user. The consequences of missing it are applied off-platform by compliance. |
| `createdAt`        | number | Unix seconds when UR created the directive.                                                                                                                                                                                                                                                  |

### fma.additional\_kyc.completed

UR sends the following when the retry session completes:

```json
{
  "event": "fma.additional_kyc.completed",
  "timestamp": 1785396060,
  "data": {
    "directiveId": "62fb1d29-e584-448f-a770-9454c94dbe24",
    "type": "retry",
    "taskType": "passport",
    "retryLevel": 6,
    "fiat24Mode": "ops_offline",
    "dataChannel": "sdk",
    "partnerId": "8509",
    "externalUserId": "your-user-id",
    "urId": 5139803526,
    "sessionId": "777b8122-c1d8-439d-8ca3-c8f977214bc1",
    "retryOfSessionId": "a67efa78-da9b-4404-a77d-97f93b8085a9",
    "completedAt": 1785396060
  }
}
```

Two `data` fields differ from the `required` payload:

* `sessionId` is the retry session that just completed, the same value `/kyc/session/create` returned to you.
* `retryOfSessionId` points at the session that the completed session replaced, so the two events carry different values in this field. That difference is expected.

`directiveId`, `taskType`, `retryLevel`, and `fiat24Mode` carry the same values in both events, so you can match them without extra lookups.

### Delivery and idempotency

Both events follow the standard delivery contract in [Webhooks](https://docs.ur.app/developer-resources/webhook): at-least-once delivery, signed with EIP-191, retried with jittered exponential backoff over roughly 48 hours until your endpoint returns 2xx.

Two points specific to retries:

* **Dedupe on `X-Webhook-Request-Id`**, as you do for every other UR event. That header is stable across the first delivery and every retry of the same message. Return 2xx only after you persist the event.
* **A resent directive does not produce a duplicate event.** UR keys outbound emission on a business key derived from the `directiveId` for `required` and from the `sessionId` for `completed`, so if UR operations resend a directive after a delivery incident, you receive the original message rather than a second one. This is UR-side suppression; it is not a substitute for your own idempotency on `X-Webhook-Request-Id`.

## 4. Claim the new session

Call `POST /api/fma/v1/kyc/session/create` after you receive `fma.additional_kyc.required`. UR consumes the pending directive and creates the session that carries the reduced step list:

{% tabs %}
{% tab title="Request" %}

```http
POST /api/fma/v1/kyc/session/create
X-External-User-Id: your-user-id
X-Ur-Id: 5139803526
Content-Type: application/json

{}
```

Send the partner signature headers you already use for every FMA endpoint. The body is optional: pass `{"externalUserId": "your-user-id"}` only when you cannot set the `X-External-User-Id` header.
{% endtab %}

{% tab title="Response" %}

```json
{
  "code": 0,
  "message": "",
  "data": { "sessionId": "777b8122-c1d8-439d-8ca3-c8f977214bc1" }
}
```

{% endtab %}
{% endtabs %}

Keep the following behaviors in mind:

* **Retrying the call is safe.** If your first call succeeded but you lost the response, calling again returns the same `sessionId` as long as that session is still active. UR does not create a second session.
* **The previous session is gone.** UR sealed it when operations created the directive. Calls that carry `retryOfSessionId` fail with `30006`.
* **One pending directive per user.** UR allows at most one pending directive for a user at a time.
* **Poll the state if you need it.** `GET /api/fma/v1/kyc/session/current` returns `sessionId`, `state`, and `dataChannel` for the active session.

## 5. What your users must do

The `taskType` field maps to the following user actions:

| `taskType`        | The user redoes                                           |
| ----------------- | --------------------------------------------------------- |
| `full`            | Every data collection step, as in first-time onboarding   |
| `address_recheck` | Address and location proof, then Form A                   |
| `passport`        | Identity document scan **and** face liveness, then Form A |
| `redo_form_a`     | Form A only; no data is recollected                       |

{% hint style="warning" %}
For `taskType: "passport"`, the user must complete the document scan **and** a fresh face liveness capture. UR resets both steps, because a new identity document paired with an earlier face capture would defeat the identity binding that the liveness step exists to prove. Tell your support staff to expect two steps, so they do not treat the liveness prompt as a defect.
{% endhint %}

### The state values a retry session reports

`GET /api/fma/v1/kyc/session/current` reports the step the user is on. A retry session runs a reduced step list, so it reports step names that never appear during first-time onboarding:

| Channel and `taskType`                   | `state` sequence                                                                                                                                       |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| SDK, `passport`                          | `IdOrPassportOrOtherIdInformationScan`, `IdAndLiveness`, `SignFormA`                                                                                   |
| SDK, `address_recheck`                   | `AddressProof`, `SignFormA`                                                                                                                            |
| SDK, `full`                              | `ConfirmYourCountryOfResidence`, `AddressProof`, `IdOrPassportOrOtherIdInformationScan`, `IdAndLiveness`, `SignFormA` (questionnaire steps interleave) |
| SDK or shared-token, `redo_form_a`       | `SignFormA` only                                                                                                                                       |
| Push, `full`                             | `PartnerDataIngestion`, `IdentityVerification`, `SignFormA`, plus `AwaitingPenny` when the user takes the penny transfer path                          |
| Push, `address_recheck` or `redo_form_a` | `SignFormA` only. The session starts at `SignFormA`; you patch the data with `/kyc/sync-data` while the state stays there                              |

{% hint style="danger" %}
Do not exhaustively branch on `state`. If your onboarding code treats the onboarding state set as closed, or fails on an unrecognized value, a retry session will break it. Treat any unrecognized `state` as "the user has a step left to do", and drive your integration from `taskType` and from the endpoint responses instead. `Completed` and `Failed` remain the only terminal values.
{% endhint %}

### Push channel

{% stepper %}
{% step %}
**Claim the session**

Call `POST /api/fma/v1/kyc/session/create` and store the returned `sessionId`.
{% endstep %}

{% step %}
**Resubmit the data**

Call `POST /api/fma/v1/kyc/sync-data`. When `requiredFields` is non-empty, send only those paths; UR merges them into the stored snapshot. When `requiredFields` is empty and `taskType` is `full`, send the complete payload, because UR starts the snapshot empty for a full retry.
{% endstep %}

{% step %}
**Complete identity verification**

Run the identity verification step your integration uses, either the penny transfer or the NFC document read. Only `taskType: "full"` includes this step on the push channel. When `state` goes straight to `SignFormA`, skip it.
{% endstep %}

{% step %}
**Sign Form A**

Call `GET /api/fma/v1/kyc/form-a-info` with the `sessionId`, present the text to the user, then call `POST /api/fma/v1/kyc/sign-form` with the `sessionId` and `textHash`.
{% endstep %}

{% step %}
**Finish the session**

Follow section 6: call `POST /api/fma/v1/kyc/submit` only when `fiat24Mode` is `auto_register`.
{% endstep %}
{% endstepper %}

### SDK channel

{% stepper %}
{% step %}
**Claim the session**

Call `POST /api/fma/v1/kyc/session/create` and store the returned `sessionId`.
{% endstep %}

{% step %}
**Request a Sumsub access token**

Call `POST /api/fma/v1/kyc/sumsub-access-token` with an empty body. Set your client timeout to **60 seconds or more**: before UR issues the token, UR resets the specific Sumsub steps the retry targets and verifies that each reset took effect, which can take up to 30 seconds. If your call still times out, the server side usually finished; call again to get the token.
{% endstep %}

{% step %}
**Launch the Sumsub SDK**

Relay the token to your app and launch the SDK. The SDK stops at the step or steps the retry targets, not the whole workflow. The user submits that data and the SDK finishes.
{% endstep %}

{% step %}
**Sign Form A**

Call `GET /api/fma/v1/kyc/form-a-info` with the `sessionId`, then `POST /api/fma/v1/kyc/sign-form` with the `sessionId` and `textHash`. Poll `GET /api/fma/v1/kyc/session/current` until `state` is `SignFormA` if you need a trigger.
{% endstep %}

{% step %}
**Finish the session**

Follow section 6: call `POST /api/fma/v1/kyc/submit` only when `fiat24Mode` is `auto_register`.
{% endstep %}
{% endstepper %}

{% hint style="info" %}
If the SDK opens and closes immediately without asking the user for anything, stop and report it through your dedicated integration channel with the `directiveId`. A retry session must always ask the user for the targeted step. An immediate completion means the data was not recollected, and the result is not valid for the compliance review that triggered the retry.
{% endhint %}

### Shared-token channel

{% stepper %}
{% step %}
**Claim the session**

Call `POST /api/fma/v1/kyc/session/create` and store the returned `sessionId`.
{% endstep %}

{% step %}
**Have the user redo KYC in your Sumsub tenant**

Run the steps that `taskType` names in your own workflow, then mint a fresh single-use share token scoped to UR's `clientId`. A token you minted for the earlier session is not valid for the retry session.
{% endstep %}

{% step %}
**Hand off the share token**

Call `POST /api/fma/v1/kyc/reuse-share-token` with the new `sessionId` and the fresh token, then read the verdict with `POST /api/fma/v1/kyc/check`.
{% endstep %}

{% step %}
**Sign Form A**

Call `GET /api/fma/v1/kyc/form-a-info`, then `POST /api/fma/v1/kyc/sign-form`.
{% endstep %}

{% step %}
**Finish the session**

Follow section 6: call `POST /api/fma/v1/kyc/submit` only when `fiat24Mode` is `auto_register`.
{% endstep %}
{% endstepper %}

## 6. How the session finishes: fiat24Mode

`fiat24Mode` reflects whether the retry session includes the Fiat24 registration step. A user can register with Fiat24 exactly once in their lifetime, so UR includes that step only for a user who never completed it:

| `fiat24Mode`    | Registration step | Your last call                   | How the session ends                                                                                                                              |
| --------------- | ----------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `auto_register` | Included          | `POST /api/fma/v1/kyc/submit`    | UR runs the registration, then sends `fma.additional_kyc.completed`                                                                               |
| `ops_offline`   | Not included      | `POST /api/fma/v1/kyc/sign-form` | The session completes on its own after the last step, then UR sends `fma.additional_kyc.completed`. UR operations handles the Fiat24 side offline |

{% hint style="warning" %}
Do not call `POST /api/fma/v1/kyc/submit` when `fiat24Mode` is `ops_offline`. The call fails with `30007`, and the session needs no submit: it completes after the last step. Branch on `fiat24Mode` rather than always calling submit.
{% endhint %}

## 7. Two fields named retryLevel

The webhook payload and `GET /api/fma/v1/kyc/session/current` both return a field named `retryLevel`, and the two values mean different things:

| Source                                                 | Meaning                                                               | Example                        |
| ------------------------------------------------------ | --------------------------------------------------------------------- | ------------------------------ |
| `fma.additional_kyc.required` and `.completed` payload | The retry category UR operations selected                             | `6` for a document rescan      |
| `GET /api/fma/v1/kyc/session/current` response         | How many sessions the user has had, where `0` is the first onboarding | `2` for the user third session |

Read the retry category from the webhook payload, and read `taskType` when you need to know what the user must do. Do not derive either one from `session/current`.

## 8. Integration checklist

Confirm the following before you handle production retries:

* Your webhook endpoint subscribes to `fma.additional_kyc.required` and `fma.additional_kyc.completed`.
* Your webhook handler is idempotent on `X-Webhook-Request-Id` and returns 2xx only after it persists the event.
* Your client timeout for `POST /api/fma/v1/kyc/sumsub-access-token` is 60 seconds or more, on the SDK channel.
* Your code branches on `fiat24Mode` and calls `/kyc/submit` only for `auto_register`.
* Your code branches on `taskType`, and your support flow expects a liveness capture whenever `taskType` is `passport`.
* Your code drops any `sessionId` you cached for the user when a retry directive arrives, and uses the `sessionId` from `/kyc/session/create`.
* Your code treats `deadlineAt` as informational and does not block the user after it passes.

## 9. Error reference

The endpoints in this flow return HTTP 200 with a business code. The following codes are specific to retry KYC:

| Code    | Meaning                                                | What to do                                                                                                                                                                                               |
| ------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `30034` | No pending retry directive for this user               | No retry is pending. Do not create a session speculatively; wait for `fma.additional_kyc.required`. Check that your signature headers identify the same `partnerId` and `externalUserId` as the webhook. |
| `30006` | `sessionId` not found                                  | The session is sealed, or it belongs to another user or partner. Claim the retry session and use its `sessionId`.                                                                                        |
| `30031` | No active KYC session for this user                    | The user has no active session. Claim the retry session first.                                                                                                                                           |
| `30007` | Session state does not allow this call                 | For `/kyc/submit`, this means the retry flow has no registration step, so the mode is `ops_offline`. Skip the submit; the session completes on its own.                                                  |
| `20007` | Form A cannot render because stored data is incomplete | On the push channel, resubmit the missing paths with `/kyc/sync-data`. On other channels, report the `directiveId` through your dedicated integration channel.                                           |
| `40003` | A UR dependency is momentarily unavailable             | Retry the call after a few seconds.                                                                                                                                                                      |

## 10. Glossary

| Term            | Definition                                                                                                     |
| --------------- | -------------------------------------------------------------------------------------------------------------- |
| Retry directive | The record UR operations creates to request a retry. Identified by `directiveId`.                              |
| Retry session   | The session UR creates when you call `/kyc/session/create` for a pending directive. Identified by `sessionId`. |
| `taskType`      | The authoritative description of what the user must redo in this retry.                                        |
| `fiat24Mode`    | Whether the retry session includes Fiat24 registration, and therefore whether you call `/kyc/submit`.          |
| Form A          | The declaration text the user signs before UR can act on the KYC data.                                         |


# Account

Account Mode APIs. Pick the mode that matches your integration.


# Managed Custody Mode

Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers.


# Get BR profile

Fetch the user's banking profile, including IBAN, fiat limits, contacts, deposit bank details, and card eligibility. Limits are denominated in CHF over a rolling 30-day window. The EUR/CHF IBAN receives EUR and CHF deposits only; a USD IBAN is provisioned separately via POST /v1/apply-usd-payin and then appears under the USD key of depositBank.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McBrResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McBrProfile"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McBrProfile":{"type":"object","properties":{"tokenId":{"type":"integer","description":"The user's URID token ID."},"br":{"type":"string","description":"Beneficiary name."},"iban":{"type":"string","description":"The user's default personal Swiss IBAN. Receives EUR and CHF deposits; it does not receive USD."},"email":{"type":"string"},"mobile":{"type":"string"},"debitCard":{"type":"string","description":"Debit card type."},"isCardEligible":{"type":"boolean","description":"Whether the user is eligible for card creation."},"cards":{"type":"array","items":{"type":"string"},"description":"List of existing cards."},"cardActivation":{"$ref":"#/components/schemas/McCardActivation"},"street":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string","description":"Country code (ISO 3166 alpha-3)."},"limits":{"$ref":"#/components/schemas/McFiatLimits"},"contacts":{"type":"object","description":"Recent payout contacts, keyed by currency.","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/McBrContact"}}},"depositBank":{"type":"object","description":"Deposit bank details, keyed by currency. The USD entry appears only after the USD IBAN is provisioned.","additionalProperties":{"$ref":"#/components/schemas/McDepositBank"}}}},"McCardActivation":{"type":"object","description":"Minimum balance required to activate a card.","properties":{"amount":{"type":"number","description":"Required activation amount."},"currency":{"type":"string","description":"Currency of the activation amount."}}},"McFiatLimits":{"type":"object","description":"Rolling 30-day fiat limits, denominated in CHF. FX, card spending, on-ramp, and payout share this bucket.","properties":{"restartDate":{"type":"string","description":"Date when the rolling window restarts."},"restartDateMs":{"type":"integer","description":"Restart timestamp in milliseconds."},"used":{"type":"number","description":"Used allowance in CHF."},"available":{"type":"number","description":"Remaining allowance in CHF. A single outgoing transaction must not exceed this value."},"max":{"type":"number","description":"Maximum allowance in CHF."}}},"McBrContact":{"type":"object","description":"A recent payout contact.","properties":{"id":{"type":"string","description":"Contact ID. Use it as contactId in the payout submission."},"name":{"type":"string","description":"Contact name."},"account":{"type":"string","description":"Masked account number."},"fullAccount":{"type":"string","description":"Full account number or IBAN."},"bank":{"type":"string","description":"Bank name."},"country":{"type":"string","description":"Country code."},"lastPaymentDate":{"type":"integer","description":"Last payment timestamp in milliseconds."}}},"McDepositBank":{"type":"object","description":"Deposit bank account details for one currency. Show the entry that matches the deposit currency to the user.","properties":{"account":{"type":"string","description":"Deposit account number or IBAN."},"bank":{"type":"string","description":"Bank name."},"BIC":{"type":"string","description":"Bank BIC."},"payee":{"type":"string","description":"Payee name."},"city":{"type":"string"},"street":{"type":"string"},"postalCode":{"type":"string"},"country":{"type":"string"}}}}},"paths":{"/api/fma/v1/br":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcGetBrProfile","summary":"Get BR profile","description":"Fetch the user's banking profile, including IBAN, fiat limits, contacts, deposit bank details, and card eligibility. Limits are denominated in CHF over a rolling 30-day window. The EUR/CHF IBAN receives EUR and CHF deposits only; a USD IBAN is provisioned separately via POST /v1/apply-usd-payin and then appears under the USD key of depositBank.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McBrResponse"}}}}}}}}}
```


# Get user balance

Fetch the user's tokenized fiat balances held inside the UR-managed account. The endpoint returns fiat only; the UR-managed account never custodies crypto.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McBalanceResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McBalanceData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McBalanceData":{"type":"object","properties":{"fiatItems":{"type":"array","description":"The user's tokenized fiat balances on Mantle (EUR24, CHF24, USD24, etc.).","items":{"$ref":"#/components/schemas/McFiatBalance"}}}},"McFiatBalance":{"type":"object","required":["currency","amount"],"properties":{"currency":{"type":"string","description":"Fiat currency symbol."},"amount":{"type":"string","description":"Balance as a decimal string."}}}}},"paths":{"/api/fma/v1/balance":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcGetBalance","summary":"Get user balance","description":"Fetch the user's tokenized fiat balances held inside the UR-managed account. The endpoint returns fiat only; the UR-managed account never custodies crypto.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McBalanceResponse"}}}}}}}}}
```


# Get account status

Read the user's UR Account status. Use it as a polling fallback after onboarding submission (1-minute cadence) or as an explicit confirmation before enabling fund-moving features; stop polling when statusStr is Live, Blocked, or Closed. The fma.account.result webhook reports final activation.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McAccountStatusResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McAccountStatusData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McAccountStatusData":{"type":"object","properties":{"status":{"type":"integer","description":"Numeric account status code."},"statusStr":{"type":"string","description":"Account status string, for example Live, Blocked, or Closed."}}}}},"paths":{"/api/fma/v1/account-status":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcGetAccountStatus","summary":"Get account status","description":"Read the user's UR Account status. Use it as a polling fallback after onboarding submission (1-minute cadence) or as an explicit confirmation before enabling fund-moving features; stop polling when statusStr is Live, Blocked, or Closed. The fma.account.result webhook reports final activation.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McAccountStatusResponse"}}}}}}}}}
```


# Apply for a USD IBAN

Request a USD deposit IBAN for a Live user. The call is synchronous; UR creates the USD IBAN immediately and it then appears under the USD key of depositBank in the BR profile. The user is identified by urId in the request body.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McApplyUsdPayinRequest":{"type":"object","required":["urId","remark"],"properties":{"urId":{"type":"integer","description":"The user's URID (numeric token ID of the URID NFT)."},"remark":{"type":"string","description":"Free-text remark for the USD IBAN application."}}},"McApplyUsdPayinResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McApplyUsdPayinData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McApplyUsdPayinData":{"type":"object","properties":{"applyId":{"type":"integer","description":"Identifier of the USD IBAN application."}}}}},"paths":{"/v1/apply-usd-payin":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcApplyUsdPayin","summary":"Apply for a USD IBAN","description":"Request a USD deposit IBAN for a Live user. The call is synchronous; UR creates the USD IBAN immediately and it then appears under the USD key of depositBank in the BR profile. The user is identified by urId in the request body.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McApplyUsdPayinRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McApplyUsdPayinResponse"}}}}}}}}}
```


# Get off-ramp quote

Request an Off-ramp quote for converting crypto on a supported source chain into the user's tokenized fiat. Pass best.to, best.swapCalldata, and best.minUsdcAmount to the Off-ramp contract exactly as returned, and submit before best.deadline. Final settlement is reported asynchronously through the transaction webhook with data.type CRYPTO\_DEPOSIT.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McOfframpQuoteRequest":{"type":"object","required":["chainId","fromToken","toCurrency","amount"],"properties":{"chainId":{"type":"string","description":"Source chain ID in CAIP-2 format. See Supported Chains and Tokens."},"fromToken":{"type":"string","description":"Source crypto token contract address."},"toCurrency":{"type":"string","description":"Target fiat currency symbol (USD, EUR, CHF, SGD, JPY, HKD)."},"amount":{"type":"string","description":"Human-readable decimal string. UR converts it to token smallest units using the source token decimals."}}},"McOfframpQuoteResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McOfframpQuoteData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McOfframpQuoteData":{"type":"object","properties":{"quoteId":{"type":"string","description":"Quote identifier."},"chainId":{"type":"string","description":"Source chain ID in CAIP-2 format."},"targetAccount":{"type":"string","description":"The user's UR Account address. Use as the _targetAccount contract parameter so the resulting fiat is credited to the user."},"best":{"$ref":"#/components/schemas/McOfframpQuoteBest"},"inputAmount":{"type":"string","description":"Input amount echoed from the request."},"outputAmount":{"type":"string","description":"Estimated fiat output amount."},"exchangeRate":{"type":"string","description":"Applied exchange rate."},"crossChainFee":{"type":"string","description":"Cross-chain fee in the source chain's native token, paid by the user in addition to the amount."},"networkFee":{"type":"string","description":"Network fee in the source chain's native token, paid by the user in addition to the amount."},"amountReceived":{"type":"string","description":"Tempo chain only: actual USDC amount received on Arbitrum, in smallest units."},"processingFee":{"type":"string","description":"Processing fee."}}},"McOfframpQuoteBest":{"type":"object","description":"Best aggregator route. Pass to, swapCalldata, and minUsdcAmount to the Off-ramp contract exactly as returned.","properties":{"aggregator":{"type":"string","description":"Aggregator name."},"to":{"type":"string","description":"Aggregator contract address. Use as the _aggregator contract parameter."},"swapCalldata":{"type":"string","description":"Swap calldata. Use as the _swapCalldata contract parameter."},"minUsdcAmount":{"type":"string","description":"Minimum USDC amount in smallest units. Use as the _minUsdcAmount contract parameter."},"expectedUsdcAmount":{"type":"string","description":"Expected USDC amount in smallest units."},"slippageBps":{"type":"integer","description":"Applied slippage in basis points."},"deadline":{"type":"integer","description":"Unix seconds. Submit the contract transaction before this deadline; otherwise it can revert."},"priceImpact":{"type":"string","description":"Estimated price impact."}}}}},"paths":{"/api/fma/v1/quote/deposit":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcGetOfframpQuote","summary":"Get off-ramp quote","description":"Request an Off-ramp quote for converting crypto on a supported source chain into the user's tokenized fiat. Pass best.to, best.swapCalldata, and best.minUsdcAmount to the Off-ramp contract exactly as returned, and submit before best.deadline. Final settlement is reported asynchronously through the transaction webhook with data.type CRYPTO_DEPOSIT.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McOfframpQuoteRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McOfframpQuoteResponse"}}}}}}}}}
```


# Get FX quote

Quote a conversion between two tokenized fiat balances held inside the user's UR account. Read the minimum and maximum FX amounts from the chain config at request time; do not hardcode them.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McFxQuoteRequest":{"type":"object","required":["fromCurrency","toCurrency","inputAmount"],"properties":{"fromCurrency":{"type":"string","description":"Input fiat currency symbol."},"toCurrency":{"type":"string","description":"Output fiat currency symbol."},"inputAmount":{"type":"string","description":"Input amount as a human-readable decimal string."}}},"McFxQuoteResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McFxQuoteData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McFxQuoteData":{"type":"object","properties":{"inputAmount":{"type":"string","description":"Input amount echoed from the request."},"fromCurrency":{"type":"string"},"toCurrency":{"type":"string"},"outputAmount":{"type":"string","description":"Quoted output amount."},"exchangeRate":{"type":"string","description":"Quoted exchange rate."}}}}},"paths":{"/api/fma/v1/quote/fx":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcGetFxQuote","summary":"Get FX quote","description":"Quote a conversion between two tokenized fiat balances held inside the user's UR account. Read the minimum and maximum FX amounts from the chain config at request time; do not hardcode them.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McFxQuoteRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McFxQuoteResponse"}}}}}}}}}
```


# Execute FX

Convert one tokenized fiat balance into another inside the user's UR account. Requires a Live UR account; FX counts against the user's rolling 30-day CHF-denominated fiat limit. The response returns only a txHash; the transaction webhook with data.type FRX reports final settlement.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McFxExchangeRequest":{"type":"object","required":["reqId","fromCurrency","toCurrency","amount"],"properties":{"reqId":{"type":"string","description":"Partner-supplied idempotency key. Keep it stable across retries of the same logical operation."},"fromCurrency":{"type":"string","description":"Input fiat currency symbol."},"toCurrency":{"type":"string","description":"Output fiat currency symbol."},"amount":{"type":"string","description":"Amount to convert, as a human-readable decimal string."},"amountOutMinimum":{"type":"string","description":"Slippage protection lower bound. If omitted, UR applies a default 0.5% slippage buffer based on the submitted amount."}}},"McTxHashResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McTxHashData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McTxHashData":{"type":"object","properties":{"txHash":{"type":"string","description":"On-chain transaction hash of the submitted operation. Submission only; final settlement arrives via the transaction webhook."}}}}},"paths":{"/api/fma/v1/fx-exchange":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcExecuteFx","summary":"Execute FX","description":"Convert one tokenized fiat balance into another inside the user's UR account. Requires a Live UR account; FX counts against the user's rolling 30-day CHF-denominated fiat limit. The response returns only a txHash; the transaction webhook with data.type FRX reports final settlement.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McFxExchangeRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McTxHashResponse"}}}}}}}}}
```


# Get payout fees

Fetch per-currency payout fee configuration, including the minimum payout amount and fiat token address. Public metadata; no authentication required.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[],"paths":{"/api/v1/banks/payout/fees":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcGetPayoutFees","summary":"Get payout fees","description":"Fetch per-currency payout fee configuration, including the minimum payout amount and fiat token address. Public metadata; no authentication required.","responses":{"200":{"description":"Standard envelope keyed by currency.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McPayoutFeesResponse"}}}}}}}},"components":{"schemas":{"McPayoutFeesResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"type":"object","description":"Payout fee configuration, keyed by currency.","additionalProperties":{"$ref":"#/components/schemas/McPayoutFeeItem"}}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McPayoutFeeItem":{"type":"object","properties":{"tokenAddress":{"type":"string","description":"Fiat token contract address on Mantle."},"currency":{"type":"string","description":"Fiat currency symbol."},"fee":{"type":"string","description":"Payout fee. Deducted from the payout amount, not charged separately."},"minimalPayoutAmount":{"type":"string","description":"Minimum payout amount for this currency."}}}}}}
```


# List banks

List recipient banks for payout contact creation, including banks in non-IBAN countries. Public metadata; no authentication required.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[],"paths":{"/api/v1/banks":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcListBanks","summary":"List banks","description":"List recipient banks for payout contact creation, including banks in non-IBAN countries. Public metadata; no authentication required.","responses":{"200":{"description":"Standard envelope. data carries the bank list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McPublicMetadataResponse"}}}}}}}},"components":{"schemas":{"McPublicMetadataResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"description":"Endpoint-specific metadata payload."}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}}}}}
```


# Get bank by IBAN

Resolve the recipient bank from an IBAN when creating a payout contact for a country that supports IBAN. Public metadata; no authentication required.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[],"paths":{"/api/v1/banks/iban/{ibanNo}":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcGetBankByIban","summary":"Get bank by IBAN","description":"Resolve the recipient bank from an IBAN when creating a payout contact for a country that supports IBAN. Public metadata; no authentication required.","parameters":[{"name":"ibanNo","in":"path","required":true,"schema":{"type":"string"},"description":"The recipient IBAN to resolve."}],"responses":{"200":{"description":"Standard envelope. data carries the resolved bank details.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McPublicMetadataResponse"}}}}}}}},"components":{"schemas":{"McPublicMetadataResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"description":"Endpoint-specific metadata payload."}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}}}}}
```


# List countries and cities

List countries and cities for collecting the payout recipient's address. Public metadata; no authentication required.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[],"paths":{"/api/v1/country-cities":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcListCountryCities","summary":"List countries and cities","description":"List countries and cities for collecting the payout recipient's address. Public metadata; no authentication required.","responses":{"200":{"description":"Standard envelope. data carries the country and city list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McPublicMetadataResponse"}}}}}}}},"components":{"schemas":{"McPublicMetadataResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"description":"Endpoint-specific metadata payload."}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}}}}}
```


# List payment purposes

List payment purposes to select from when creating or verifying a payout contact. Public metadata; no authentication required.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[],"paths":{"/api/v1/payment-purposes":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcListPaymentPurposes","summary":"List payment purposes","description":"List payment purposes to select from when creating or verifying a payout contact. Public metadata; no authentication required.","responses":{"200":{"description":"Standard envelope. data carries the payment purpose list.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McPublicMetadataResponse"}}}}}}}},"components":{"schemas":{"McPublicMetadataResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"description":"Endpoint-specific metadata payload."}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}}}}}
```


# Verify reference

Validate a new payment reference for an existing recent contact and get a fresh refId and purposeId to use in the payout submission.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McVerifyReferenceRequest":{"type":"object","required":["reference"],"properties":{"reference":{"type":"string","description":"The payment reference to validate."}}},"McVerifyReferenceResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McPayoutRefParams"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McPayoutRefParams":{"type":"object","properties":{"contactId":{"type":"string","description":"Contact ID to use in the payout submission."},"purposeId":{"type":"integer","description":"Payment purpose ID."},"refId":{"type":"string","description":"Reference ID. Provide together with purposeId in the payout submission."}}}}},"paths":{"/api/fma/v1/verify-reference":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcVerifyReference","summary":"Verify reference","description":"Validate a new payment reference for an existing recent contact and get a fresh refId and purposeId to use in the payout submission.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McVerifyReferenceRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McVerifyReferenceResponse"}}}}}}}}}
```


# Verify contact

Create and validate a new payout recipient, returning the contactId, purposeId, and refId needed to submit the payout. Creditor name, street, city, and country must use Latin characters.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McVerifyContactRequest":{"type":"object","required":["account","bankName","bic","purpose","reference"],"properties":{"account":{"type":"string","description":"Recipient IBAN or account number."},"bankName":{"type":"string","description":"Recipient bank name."},"bic":{"type":"string","description":"Recipient bank BIC."},"purpose":{"type":"integer","description":"Payment purpose ID from the payment purposes list."},"reference":{"type":"string","description":"Payment reference."},"creditorInfo":{"$ref":"#/components/schemas/McCreditorInfo"}}},"McCreditorInfo":{"type":"object","description":"Recipient details, required when creating a new third-party contact. name, street, city, and country must use Latin characters.","required":["name","street","city","zip","country"],"properties":{"name":{"type":"string"},"street":{"type":"string"},"city":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string"}}},"McVerifyContactResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McVerifyContactData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McVerifyContactData":{"type":"object","properties":{"account":{"type":"string","description":"Recipient IBAN or account number echoed from the request."},"bankName":{"type":"string"},"bic":{"type":"string"},"purpose":{"type":"integer"},"reference":{"type":"string"},"clientPayoutRefParams":{"$ref":"#/components/schemas/McPayoutRefParams"}}},"McPayoutRefParams":{"type":"object","properties":{"contactId":{"type":"string","description":"Contact ID to use in the payout submission."},"purposeId":{"type":"integer","description":"Payment purpose ID."},"refId":{"type":"string","description":"Reference ID. Provide together with purposeId in the payout submission."}}}}},"paths":{"/api/fma/v1/verify-contact":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcVerifyContact","summary":"Verify contact","description":"Create and validate a new payout recipient, returning the contactId, purposeId, and refId needed to submit the payout. Creditor name, street, city, and country must use Latin characters.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McVerifyContactRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McVerifyContactResponse"}}}}}}}}}
```


# Submit payout

Send tokenized fiat from the user's UR account to an external bank account via SEPA or SWIFT. Requires a Live UR account; fees are deducted from the amount, and the payout counts against the rolling 30-day CHF-denominated fiat limit. The response returns only a txHash; the transaction webhook with data.type FIAT\_WITHDRAW reports final settlement.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McSubmitPayoutRequest":{"type":"object","required":["reqId","amount","contactId","currency","metadata"],"properties":{"reqId":{"type":"string","description":"Partner-supplied idempotency key. Keep it stable across retries of the same logical operation."},"amount":{"type":"string","description":"Payout amount as a string. Minimum is currency-specific; see minimalPayoutAmount from the payout fees endpoint. Network and payout fees are deducted from this amount."},"contactId":{"type":"string","description":"Contact ID from the BR profile contacts or from verify-contact."},"currency":{"type":"string","description":"Payout fiat currency symbol."},"purposeId":{"type":"string","description":"Payment purpose ID as a decimal string. Provide together with refId, or omit both."},"refId":{"type":"string","description":"Reference ID from verify-reference or verify-contact. Provide together with purposeId, or omit both."},"metadata":{"$ref":"#/components/schemas/McPayoutMetadata"}}},"McPayoutMetadata":{"type":"object","description":"Additional bank details. Name, address, and reference values must use Latin characters.","properties":{"bankAccountHolder":{"type":"string","description":"Recipient account holder name."},"bankName":{"type":"string","description":"Recipient bank name."},"bankAccount":{"type":"string","description":"Recipient account number or IBAN."},"bankReference":{"type":"string","description":"Bank reference."}}},"McTxHashResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McTxHashData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McTxHashData":{"type":"object","properties":{"txHash":{"type":"string","description":"On-chain transaction hash of the submitted operation. Submission only; final settlement arrives via the transaction webhook."}}}}},"paths":{"/api/fma/v1/submit-payout":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcSubmitPayout","summary":"Submit payout","description":"Send tokenized fiat from the user's UR account to an external bank account via SEPA or SWIFT. Requires a Live UR account; fees are deducted from the amount, and the payout counts against the rolling 30-day CHF-denominated fiat limit. The response returns only a txHash; the transaction webhook with data.type FIAT_WITHDRAW reports final settlement.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McSubmitPayoutRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McTxHashResponse"}}}}}}}}}
```


# Get on-ramp limit

Check On-ramp availability and per-currency amount limits before starting the flow. Block the flow when regionLocked, usdcDepegged, or livenessLocked is true, or when the requested amount falls outside the minAmounts to maxAmounts range. On-ramp is not yet available for integration; this reference is a preview.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McOnrampLimitResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McOnrampLimitData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McOnrampLimitData":{"type":"object","properties":{"livenessLocked":{"type":"boolean","description":"True when the user is temporarily locked out of liveness checks. Block the flow when true."},"livenessLockMins":{"type":"integer","description":"Remaining liveness lock duration in minutes."},"maxAmounts":{"type":"object","description":"Maximum amounts keyed by fiat currency. Derived from a USD anchor at the live rate; read at request time, do not hardcode.","additionalProperties":{"type":"string"}},"minAmounts":{"type":"object","description":"Minimum amounts keyed by fiat currency. About 5 USD equivalent; read at request time, do not hardcode.","additionalProperties":{"type":"string"}},"usdcDepegged":{"type":"boolean","description":"True when USDC is depegged. Block the flow when true."},"regionLocked":{"type":"boolean","description":"True when the user's region is restricted. Block the flow when true."}}}}},"paths":{"/api/fma/v1/onramp-limit":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcGetOnrampLimit","summary":"Get on-ramp limit","description":"Check On-ramp availability and per-currency amount limits before starting the flow. Block the flow when regionLocked, usdcDepegged, or livenessLocked is true, or when the requested amount falls outside the minAmounts to maxAmounts range. On-ramp is not yet available for integration; this reference is a preview.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McOnrampLimitResponse"}}}}}}}}}
```


# Check pending on-ramp retry

Read the user's pending On-ramp retry, an On-ramp whose bridge leg succeeded but whose swap leg failed. An empty originalTxHash means no pending retry. While a pending retry exists, block new On-ramp submissions until the user retries or cancels. On-ramp is not yet available for integration; this reference is a preview.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McOnrampPendingRetryResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McOnrampPendingRetryData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McOnrampPendingRetryData":{"type":"object","properties":{"originalTxHash":{"type":"string","description":"Original On-ramp transaction hash. Empty when there is no pending retry."},"originalChainId":{"type":"string","description":"Source chain ID of the original On-ramp, in CAIP-2 format."},"originalCurrency":{"type":"string","description":"Original source fiat currency symbol."},"chainId":{"type":"string","description":"Destination chain ID in CAIP-2 format. Use as srcChainId and dstChainId in the retry quote."},"fromToken":{"type":"string","description":"Bridge intermediate token (USDC) address on the destination chain."},"toToken":{"type":"string","description":"Destination token address."},"amount":{"type":"string","description":"Stuck USDC amount as a human-readable decimal string. Use as the retry quote amount."},"amountRaw":{"type":"string","description":"Stuck USDC amount in smallest units. Use as usdcAmount in the retry submission."},"failedAt":{"type":"integer","description":"Failure timestamp in Unix seconds."}}}}},"paths":{"/api/fma/v1/onramp/pending-retry":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcGetOnrampPendingRetry","summary":"Check pending on-ramp retry","description":"Read the user's pending On-ramp retry, an On-ramp whose bridge leg succeeded but whose swap leg failed. An empty originalTxHash means no pending retry. While a pending retry exists, block new On-ramp submissions until the user retries or cancels. On-ramp is not yet available for integration; this reference is a preview.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McOnrampPendingRetryResponse"}}}}}}}}}
```


# Get on-ramp quote

Quote an On-ramp (scene onramp) or a retry swap (scene swap\_retry). When the response has needLiveness true, complete the liveness check before submitting; after liveness passes, request a new quote and submit with the new quoteId. On-ramp is not yet available for integration; this reference is a preview.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McOnrampQuoteRequest":{"type":"object","required":["scene","dstChainId","toToken","amount"],"properties":{"scene":{"type":"string","enum":["onramp","swap_retry"],"description":"onramp for the main flow; swap_retry for retrying a failed swap leg."},"srcChainId":{"type":"string","description":"Source chain ID in CAIP-2 format; the chain where the user's tokenized fiat is held. For swap_retry, use the pending retry's chainId."},"dstChainId":{"type":"string","description":"Destination chain ID in CAIP-2 format. Must match a token with aggregator support in the chain config."},"fromCurrency":{"type":"string","description":"Source fiat currency symbol. Required when scene is onramp."},"fromToken":{"type":"string","description":"USDC token address. Required when scene is swap_retry."},"toToken":{"type":"string","description":"Destination token address."},"amount":{"type":"string","description":"Input amount as a human-readable decimal string; UR converts it to smallest units by the input currency decimals."},"slippageBps":{"type":"integer","description":"Slippage tolerance in basis points. Defaults to 50."}}},"McOnrampQuoteResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McOnrampQuoteData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McOnrampQuoteData":{"type":"object","properties":{"quoteId":{"type":"string","description":"Quote identifier. Echo it in the On-ramp submission before it expires."},"srcChainId":{"type":"string"},"dstChainId":{"type":"string"},"needLiveness":{"type":"boolean","description":"True when the user must pass a liveness check before this quote can be submitted."},"best":{"$ref":"#/components/schemas/McOnrampQuoteBest"},"inputAmount":{"type":"string","description":"Input fiat amount."},"outputAmount":{"type":"string","description":"Estimated output token amount."},"exchangeRate":{"type":"string","description":"Applied exchange rate."},"crossChainFee":{"type":"string","description":"Cross-chain fee component."},"networkFee":{"type":"string","description":"Destination-chain gas plus cross-chain fee, deducted from the user's input fiat."},"processingFee":{"type":"string","description":"Processing fee."},"totalFee":{"type":"string","description":"Total fee."},"warningMessage":{"type":"string","description":"Warning text, when applicable."}}},"McOnrampQuoteBest":{"type":"object","description":"Best aggregator route for the destination-chain swap.","properties":{"aggregator":{"type":"string","description":"Aggregator name. Do not use as the retry aggregator parameter; use to instead."},"to":{"type":"string","description":"Aggregator contract address. Use as dstAggregator (submit) or aggregator (retry)."},"swapCalldata":{"type":"string","description":"Swap calldata. Use as dstSwapCalldata (submit) or swapCalldata (retry)."},"minUsdcAmount":{"type":"string","description":"Minimum USDC amount in smallest units."},"expectedUsdcAmount":{"type":"string","description":"Expected USDC amount in smallest units."},"minAmountOut":{"type":"string","description":"Minimum output amount in smallest units. Use as dstMinAmountOut (submit) or minAmountOut (retry)."},"expectedAmountOut":{"type":"string","description":"Expected output amount in smallest units."},"slippageBps":{"type":"integer","description":"Applied slippage in basis points."},"deadline":{"type":"integer","description":"Quote deadline in Unix seconds."},"priceImpact":{"type":"string","description":"Estimated price impact."}}}}},"paths":{"/api/fma/v1/quote/onramp":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcGetOnrampQuote","summary":"Get on-ramp quote","description":"Quote an On-ramp (scene onramp) or a retry swap (scene swap_retry). When the response has needLiveness true, complete the liveness check before submitting; after liveness passes, request a new quote and submit with the new quoteId. On-ramp is not yet available for integration; this reference is a preview.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McOnrampQuoteRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McOnrampQuoteResponse"}}}}}}}}}
```


# Get on-ramp liveness token

Issue a vendor access token for the liveness check. Only call this endpoint when an On-ramp quote returns needLiveness true. On-ramp is not yet available for integration; this reference is a preview.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McLivenessTokenResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McLivenessTokenData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McLivenessTokenData":{"type":"object","properties":{"vendor":{"type":"string","description":"Liveness vendor identifier, for example sumsub."},"access_token":{"type":"string","description":"Vendor access token for running the liveness check."},"user_id":{"type":"string","description":"Vendor user identifier."}}}}},"paths":{"/api/fma/v1/onramp-liveness-token":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcGetOnrampLivenessToken","summary":"Get on-ramp liveness token","description":"Issue a vendor access token for the liveness check. Only call this endpoint when an On-ramp quote returns needLiveness true. On-ramp is not yet available for integration; this reference is a preview.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McLivenessTokenResponse"}}}}}}}}}
```


# Get on-ramp liveness result

Poll the result of the liveness check. After liveness\_result is pass, request a new On-ramp quote and submit using the new quoteId. On-ramp is not yet available for integration; this reference is a preview.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McLivenessResultResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McLivenessResultData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McLivenessResultData":{"type":"object","properties":{"liveness_result":{"type":"string","description":"Liveness check outcome, e.g. pass or pending."},"checked_at":{"type":"integer","description":"Check timestamp in Unix seconds."},"expired_at":{"type":"integer","description":"Result expiry timestamp in Unix seconds."},"liveness_fail_reason":{"type":"string","description":"Failure reason; empty on pass."},"liveness_locked":{"type":"boolean","description":"True when the user is locked out after repeated failures."},"liveness_unlock_at":{"type":"integer","description":"Unlock timestamp in Unix seconds; 0 when not locked."}}}}},"paths":{"/api/fma/v1/onramp-liveness-result":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcGetOnrampLivenessResult","summary":"Get on-ramp liveness result","description":"Poll the result of the liveness check. After liveness_result is pass, request a new On-ramp quote and submit using the new quoteId. On-ramp is not yet available for integration; this reference is a preview.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McLivenessResultResponse"}}}}}}}}}
```


# Submit on-ramp

Convert the user's tokenized fiat into crypto delivered to an external wallet. Requires a Live UR account, a valid unexpired quoteId, passed liveness when the quote required it, and no pending retry. The response returns only a txHash; the transaction webhook with data.type ONRAMP reports the final result. On-ramp is not yet available for integration; this reference is a preview.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McOnrampRequest":{"type":"object","required":["reqId","quoteId","fromCurrency","amountIn","withdrawAddress"],"properties":{"reqId":{"type":"string","description":"Partner-supplied idempotency key. Keep it stable across retries of the same logical operation."},"quoteId":{"type":"string","description":"quoteId from the On-ramp quote. Must match UR's cached quote and not be expired."},"fromCurrency":{"type":"string","description":"Source fiat currency symbol."},"chainId":{"type":"string","description":"Source chain ID in CAIP-2 format."},"amountIn":{"type":"string","description":"Input amount as a human-readable decimal string. Must match the amount used for the cached quote."},"dstChainId":{"type":"string","description":"Destination chain ID in CAIP-2 format."},"withdrawAddress":{"type":"string","description":"External wallet that receives the crypto. Required for every On-ramp; the UR-managed account holds fiat only and never receives crypto."},"dstAggregator":{"type":"string","description":"best.to from the quote."},"dstTokenOut":{"type":"string","description":"Destination token address."},"dstSwapCalldata":{"type":"string","description":"best.swapCalldata from the quote."},"dstMinAmountOut":{"type":"string","description":"best.minAmountOut from the quote."}}},"McTxHashResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McTxHashData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McTxHashData":{"type":"object","properties":{"txHash":{"type":"string","description":"On-chain transaction hash of the submitted operation. Submission only; final settlement arrives via the transaction webhook."}}}}},"paths":{"/api/fma/v1/onramp":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcSubmitOnramp","summary":"Submit on-ramp","description":"Convert the user's tokenized fiat into crypto delivered to an external wallet. Requires a Live UR account, a valid unexpired quoteId, passed liveness when the quote required it, and no pending retry. The response returns only a txHash; the transaction webhook with data.type ONRAMP reports the final result. On-ramp is not yet available for integration; this reference is a preview.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McOnrampRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McTxHashResponse"}}}}}}}}}
```


# Retry on-ramp swap

Redo the swap leg of an On-ramp whose bridge succeeded but whose swap failed. Only call this endpoint when the pending-retry endpoint returns a pending item, and use a fresh quote with scene swap\_retry. The transaction webhook reports the final result. On-ramp is not yet available for integration; this reference is a preview.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McOnrampSwapRequest":{"type":"object","required":["reqId","quoteId","chainId","originalTxHash","usdcAmount","tokenOut","minAmountOut","aggregator","swapCalldata"],"properties":{"reqId":{"type":"string","description":"Partner-supplied idempotency key. Keep it stable across retries of the same logical operation."},"quoteId":{"type":"string","description":"quoteId from a fresh quote with scene swap_retry."},"chainId":{"type":"string","description":"Destination chain ID in CAIP-2 format, from the pending retry."},"originalTxHash":{"type":"string","description":"Original On-ramp transaction hash, from the pending retry."},"usdcAmount":{"type":"string","description":"Stuck USDC amount in smallest units (pending retry amountRaw)."},"tokenOut":{"type":"string","description":"Destination token address (pending retry toToken)."},"minAmountOut":{"type":"string","description":"best.minAmountOut from the retry quote."},"aggregator":{"type":"string","description":"best.to from the retry quote (not best.aggregator)."},"swapCalldata":{"type":"string","description":"best.swapCalldata from the retry quote."}}},"McTxHashResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McTxHashData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McTxHashData":{"type":"object","properties":{"txHash":{"type":"string","description":"On-chain transaction hash of the submitted operation. Submission only; final settlement arrives via the transaction webhook."}}}}},"paths":{"/api/fma/v1/onramp-swap":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcRetryOnrampSwap","summary":"Retry on-ramp swap","description":"Redo the swap leg of an On-ramp whose bridge succeeded but whose swap failed. Only call this endpoint when the pending-retry endpoint returns a pending item, and use a fresh quote with scene swap_retry. The transaction webhook reports the final result. On-ramp is not yet available for integration; this reference is a preview.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McOnrampSwapRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McTxHashResponse"}}}}}}}}}
```


# Cancel on-ramp retry

Clear the user's pending On-ramp retry record. A successful response allows the normal On-ramp entry point to be re-enabled. On-ramp is not yet available for integration; this reference is a preview.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McOnrampRetryCancelRequest":{"type":"object","required":["originalTxHash"],"properties":{"originalTxHash":{"type":"string","description":"Original On-ramp transaction hash of the pending retry to cancel."}}},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}}}},"paths":{"/api/fma/v1/onramp/retry/cancel":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcCancelOnrampRetry","summary":"Cancel on-ramp retry","description":"Clear the user's pending On-ramp retry record. A successful response allows the normal On-ramp entry point to be re-enabled. On-ramp is not yet available for integration; this reference is a preview.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McOnrampRetryCancelRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McBaseResponse"}}}}}}}}}
```


# Create card

Create a virtual card for an eligible Live user. Requires isCardEligible true in the BR profile and a balance that satisfies cardActivation.amount in cardActivation.currency.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}}}},"paths":{"/api/fma/v1/open-card":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcCreateCard","summary":"Create card","description":"Create a virtual card for an eligible Live user. Requires isCardEligible true in the BR profile and a balance that satisfies cardActivation.amount in cardActivation.currency.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Empty JSON object."}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McBaseResponse"}}}}}}}}}
```


# Get card info

Fetch card metadata and a short-lived cardToken for secure card display. The response never exposes real PAN, CVV, or expiry; render them through UR's card display script using cardToken, which expires after 5 minutes.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McCardResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McCardData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McCardData":{"type":"object","properties":{"security":{"$ref":"#/components/schemas/McCardSecurity"},"currencies":{"type":"array","items":{"type":"string"},"description":"Currencies available for card transactions."},"tokenId":{"type":"integer","description":"The user's URID token ID."},"limits":{"$ref":"#/components/schemas/McCardLimits"},"cardDesign":{"type":"string"},"cardHolder":{"type":"string"},"status":{"type":"string","description":"Card status, for example Active."},"currency":{"type":"string","description":"Default card transaction currency."},"masked":{"$ref":"#/components/schemas/McCardMasked"},"cardToken":{"type":"string","description":"Short-lived token for card detail display only. Expires after 5 minutes. Do not store or log it."},"activeTokens":{"type":"array","items":{"$ref":"#/components/schemas/McCardTokenInfo"}},"inactiveTokens":{"type":"array","items":{"$ref":"#/components/schemas/McCardTokenInfo"}},"externalId":{"type":"string","description":"Stable card management ID. Use it for card management APIs."}}},"McCardSecurity":{"type":"object","properties":{"contactlessEnabled":{"type":"boolean"},"withdrawalEnabled":{"type":"boolean"},"internetPurchaseEnabled":{"type":"boolean"},"overallLimitsEnabled":{"type":"boolean"}}},"McCardLimits":{"type":"object","properties":{"account":{"$ref":"#/components/schemas/McCardLimitItem"},"withdrawal":{"$ref":"#/components/schemas/McCardLimitItem"},"internetPurchase":{"$ref":"#/components/schemas/McCardLimitItem"}}},"McCardLimitItem":{"type":"object","properties":{"restartDate":{"type":"string","description":"Date when the limit window restarts."},"restartDateMs":{"type":"integer","description":"Restart timestamp in milliseconds."},"used":{"type":"number","description":"Used allowance."},"available":{"type":"number","description":"Remaining allowance."},"max":{"type":"number","description":"Maximum allowance."}}},"McCardMasked":{"type":"object","description":"Masked card fields. Real PAN, CVV, and expiry are rendered only through UR's card display script.","properties":{"cardNumber":{"type":"string"},"cvv2":{"type":"string"},"expiry":{"type":"string"}}},"McCardTokenInfo":{"type":"object","description":"Device wallet token, such as Apple Pay. Do not use its id for card detail display or currency settings.","properties":{"id":{"type":"string"},"type":{"type":"string"},"createdAt":{"type":"string"}}}}},"paths":{"/api/fma/v1/card":{"get":{"tags":["Managed Custody Mode"],"operationId":"mcGetCardInfo","summary":"Get card info","description":"Fetch card metadata and a short-lived cardToken for secure card display. The response never exposes real PAN, CVV, or expiry; render them through UR's card display script using cardToken, which expires after 5 minutes.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McCardResponse"}}}}}}}}}
```


# Set default card currency

Set the user's default card transaction currency. UR resolves the card's externalId from the authenticated user; the setting affects the default refund currency display and debit preference.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McSetCardCurrencyRequest":{"type":"object","required":["currency"],"properties":{"currency":{"type":"string","description":"Target default transaction currency, such as USD, EUR, or CHF."}}},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}}}},"paths":{"/api/fma/v1/card-currency":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcSetCardCurrency","summary":"Set default card currency","description":"Set the user's default card transaction currency. UR resolves the card's externalId from the authenticated user; the setting affects the default refund currency display and debit preference.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McSetCardCurrencyRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McBaseResponse"}}}}}}}}}
```


# Update card status

Block or unblock the user's card. UR resolves the card's externalId from the authenticated user; a blocked card declines all authorization attempts until unblocked.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McUpdateCardStatusRequest":{"type":"object","required":["statusChange"],"properties":{"statusChange":{"type":"string","enum":["block","unblock"],"description":"The status transition to apply."}}},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}}}},"paths":{"/api/fma/v1/update-card-status":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcUpdateCardStatus","summary":"Update card status","description":"Block or unblock the user's card. UR resolves the card's externalId from the authenticated user; a blocked card declines all authorization attempts until unblocked.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McUpdateCardStatusRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McBaseResponse"}}}}}}}}}
```


# Fetch transaction history

Fetch paginated transaction history for reconciliation, or fetch a single transaction by sending exactly one of id, txHash, or reqId (mutually exclusive). The transaction webhook delivers the same transaction structure; parse detailsJson according to the transaction type.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"Managed Custody Mode","description":"Server-to-server APIs where UR custodies user funds. Partner Auth (EIP-191) with user identity headers."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"McTransactionsRequest":{"type":"object","properties":{"pageSize":{"type":"integer","description":"Page size. Defaults to 50; maximum is 100."},"type":{"type":"string","description":"Single transaction type filter. Do not send together with txTypes."},"txTypes":{"type":"array","items":{"type":"string"},"description":"Multiple transaction type filter. Do not send together with type."},"currencies":{"type":"array","items":{"type":"string"},"description":"Currency filter. Values are normalized to lowercase by UR."},"direction":{"type":"string","enum":["IN","OUT","ALL"],"description":"Direction filter."},"status":{"type":"string","description":"Transaction status filter."},"chainId":{"type":"string","description":"Chain ID filter, for example eip155:5000."},"tokenSymbol":{"type":"string","description":"Token symbol filter, for example USDC."},"minAmount":{"type":"string","description":"Minimum amount filter."},"maxAmount":{"type":"string","description":"Maximum amount filter."},"fromTimestamp":{"type":"integer","description":"Start timestamp filter."},"toTimestamp":{"type":"integer","description":"End timestamp filter."},"cursorTimestamp":{"type":"integer","description":"Next-page cursor timestamp from data.nextCursor.timestamp. Send only when hasNextPage is true."},"cursorId":{"type":"integer","description":"Next-page cursor ID from data.nextCursor.id. Send only when hasNextPage is true."},"prevCursorTimestamp":{"type":"integer","description":"Previous-page cursor timestamp from data.prevCursor.timestamp. Send only when hasPrevPage is true."},"prevCursorId":{"type":"integer","description":"Previous-page cursor ID from data.prevCursor.id. Send only when hasPrevPage is true."},"id":{"type":"integer","description":"Exact lookup by UR internal transaction ID. Mutually exclusive with txHash and reqId."},"txHash":{"type":"string","description":"Exact lookup by transaction hash. Mutually exclusive with id and reqId."},"reqId":{"type":"string","description":"Exact lookup by the idempotency key used when the transaction was created. Mutually exclusive with id and txHash."}}},"McTransactionsResponse":{"allOf":[{"$ref":"#/components/schemas/McBaseResponse"},{"type":"object","properties":{"data":{"$ref":"#/components/schemas/McTransactionsData"}}}]},"McBaseResponse":{"type":"object","description":"Standard UR OpenAPI response envelope. code 0 means success; a non-zero code is a business error described by message.","required":["code","message"],"properties":{"code":{"type":"integer","description":"0 on success; non-zero business error code."},"message":{"type":"string","description":"Human-readable explanation. May be empty on success."}}},"McTransactionsData":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/McTransactionItem"}},"hasNextPage":{"type":"boolean","description":"True when a next page exists. Only then send nextCursor fields."},"hasPrevPage":{"type":"boolean","description":"True when a previous page exists. Only then send prevCursor fields; a zero cursor means there is no previous page."},"nextCursor":{"$ref":"#/components/schemas/McTransactionCursor"},"prevCursor":{"$ref":"#/components/schemas/McTransactionCursor"},"currentPageSize":{"type":"integer","description":"Number of items in this page."}}},"McTransactionItem":{"type":"object","properties":{"id":{"type":"integer","description":"UR internal transaction ID."},"txHash":{"type":"string","description":"On-chain transaction hash."},"txLogIndex":{"type":"integer","description":"Transaction log index."},"blockNumber":{"type":"integer","description":"Block number."},"createTimeE9":{"type":"integer","description":"Creation time in nanoseconds."},"broadcastTimeE9":{"type":"integer","description":"Broadcast time in nanoseconds."},"finalTimeE9":{"type":"integer","description":"Finalization time in nanoseconds."},"type":{"type":"string","enum":["CRYPTO_DEPOSIT","INTERNAL_TOKEN_TRANSFER","UNKNOWN","FX_EXCHANGE","MARQETA_AUTHORIZE","FIAT_WITHDRAW","FIAT_DEPOSIT","ONRAMP"],"description":"Transaction type. Determines the structure of detailsJson."},"chainId":{"type":"string","description":"Chain ID, for example eip155:5000."},"chainName":{"type":"string","description":"Chain name, for example Mantle."},"urId":{"type":"string","description":"The user's URID as a string."},"direction":{"type":"string","enum":["IN","OUT"],"description":"Transaction direction."},"amount":{"type":"string","description":"Signed amount as a decimal string."},"currency":{"type":"string","description":"Currency in lowercase."},"status":{"type":"string","enum":["UNKNOWN","INIT","PENDING","CONFIRMED","FAILED","PENDING_DROP","DROPPED","REJECTED"],"description":"Transaction status. CONFIRMED means settled on-chain; REJECTED means UR's validation or compliance checks rejected it."},"reqId":{"type":"string","description":"Idempotency key used when the transaction was created."},"detailsJson":{"type":"string","description":"Stringified JSON whose structure depends on type. Parse the string before reading nested fields."},"refundType":{"type":"string","enum":["","BANK_REFUND","CARD_REFUND","CARD_REVERSAL"],"description":"Refund discriminator. Empty string means not a refund."}}},"McTransactionCursor":{"type":"object","properties":{"timestamp":{"type":"integer","description":"Cursor timestamp."},"id":{"type":"integer","description":"Cursor ID."}}}}},"paths":{"/api/fma/v1/transactions":{"post":{"tags":["Managed Custody Mode"],"operationId":"mcFetchTransactions","summary":"Fetch transaction history","description":"Fetch paginated transaction history for reconciliation, or fetch a single transaction by sending exactly one of id, txHash, or reqId (mutually exclusive). The transaction webhook delivers the same transaction structure; parse detailsJson according to the transaction type.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed 65-byte hex EIP-191 signature over the Partner Auth message. See the Signature and verify guide."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds. UR rejects the request when the current time is past the deadline. Keep the validity window at or under 5 minutes."},{"name":"X-Api-PublicKey","in":"header","required":true,"schema":{"type":"string"},"description":"0x-prefixed partner signer address registered with UR."},{"name":"X-Ur-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."},{"name":"X-External-User-Id","in":"header","required":false,"schema":{"type":"string"},"description":"Send at least one of X-Ur-Id or X-External-User-Id. When both are present, UR resolves the user by X-Ur-Id first."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McTransactionsRequest"}}}},"responses":{"200":{"description":"Standard envelope. Business errors return HTTP 200 with a non-zero code. For exact lookups, data.items is empty when no transaction matches.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McTransactionsResponse"}}}}}}}}}
```


# External Wallet Access Mode

APIs signed by the user's own wallet key. Base URL <https://api.ur.app>.


# Get server timestamp

Returns the current UR server timestamp. Use it to compute the signature deadline for Full Auth requests. No authentication required.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[],"paths":{"/api/v1/timestamp":{"get":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetTimestamp","summary":"Get server timestamp","description":"Returns the current UR server timestamp. Use it to compute the signature deadline for Full Auth requests. No authentication required.","responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}},"components":{"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}}}
```


# Check email availability

Checks whether an email address is available for registration. Returns retCode 10019 when the email is already registered. No authentication required.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[],"paths":{"/api/v2/email-status":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaCheckEmailStatus","summary":"Check email availability","description":"Checks whether an email address is available for registration. Returns retCode 10019 when the email is already registered. No authentication required.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","description":"Email address to check."}},"required":["email"]}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}},"components":{"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}}}
```


# Get user account status

Returns the account status, KYC flow progress, Sumsub KYC info, novice guidance progress, and CRS requirements. Status codes: 0 Na, 1 SoftBlocked, 2 Tourist, 3 Blocked, 4 Closed, 5 Live.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[],"paths":{"/api/v2/account-status":{"get":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetAccountStatus","summary":"Get user account status","description":"Returns the account status, KYC flow progress, Sumsub KYC info, novice guidance progress, and CRS requirements. Status codes: 0 Na, 1 SoftBlocked, 2 Tourist, 3 Blocked, 4 Closed, 5 Live.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."}],"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}},"components":{"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}}}
```


# Mint the user's URID

Mints the URID NFT for a new user (account status must be Na). Requires GeeTest verification fields. The tokenId header carries the pre-generated URID.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[],"paths":{"/api/v1/mint":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaMintUrid","summary":"Mint the user's URID","description":"Mints the URID NFT for a new user (account status must be Na). Requires GeeTest verification fields. The tokenId header carries the pre-generated URID.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"address":{"type":"string","description":"User's Ethereum wallet address."},"lotNumber":{"type":"string","description":"GeeTest verification session ID."},"captchaOutput":{"type":"string","description":"GeeTest verification output result."},"passToken":{"type":"string","description":"GeeTest pass token."},"genTime":{"type":"string","description":"GeeTest generation timestamp (seconds)."},"image":{"type":"string","description":"NFT image (Base64). Optional."},"backGroundColor":{"type":"string","description":"Background color (hex). Optional."},"land":{"type":"string","description":"Land information. Optional."}},"required":["address","lotNumber","captchaOutput","passToken","genTime"]}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}},"components":{"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}}}
```


# Create a Sumsub SDK token

Creates an access token to initialize the Sumsub SDK for KYC verification. The NFC scan step requires the Sumsub mobile SDK; it is not available in the web SDK. Omit userId unless UR gave you an explicit tenant pattern.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v1/sumsub/create-access-token":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaCreateSumsubAccessToken","summary":"Create a Sumsub SDK token","description":"Creates an access token to initialize the Sumsub SDK for KYC verification. The NFC scan step requires the Sumsub mobile SDK; it is not available in the web SDK. Omit userId unless UR gave you an explicit tenant pattern.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","description":"Optional. Server derives the Sumsub user id from Full Auth context when omitted."},"levelName":{"type":"string","description":"Specific Sumsub level name. Empty uses the default."},"ttl":{"type":"integer","description":"Token time-to-live in seconds. Default configured by the server."},"isRetryVerification":{"type":"boolean","description":"True for KYC retry scenarios."},"retryLevel":{"type":"integer","description":"Retry level (1-7); valid only when isRetryVerification is true."},"stepType":{"type":"string","description":"Specific verification step to reset, e.g. IDENTITY."},"failureReason":{"type":"string","description":"Failure reason, used for logging."}}}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Get Form A text

Returns the Form A (customer due diligence) declaration text the user must read and sign after Sumsub verification. The user signs this exact text with their wallet.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v2/kyc/form-a-info":{"get":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetFormAInfo","summary":"Get Form A text","description":"Returns the Form A (customer due diligence) declaration text the user must read and sign after Sumsub verification. The user signs this exact text with their wallet.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Submit signed Form A

Submits the user's EIP-191 wallet signature over the exact Form A text from form-a-info. Completes the final KYC step. After success, allow about 3 seconds for downstream status updates.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v2/kyc/submit-form-a":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaSubmitFormA","summary":"Submit signed Form A","description":"Submits the user's EIP-191 wallet signature over the exact Form A text from form-a-info. Completes the final KYC step. After success, allow about 3 seconds for downstream status updates.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kycSelfDec":{"type":"string","description":"Form A text. Must exactly match the kycSelfDec returned by form-a-info."},"kycSelfDecSign":{"type":"string","description":"EIP-191 signature of the Form A text (65-byte hex, 0x prefix)."}},"required":["kycSelfDec","kycSelfDecSign"]}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Get user banking profile

Returns the user's banking profile: IBAN, holder name, address, card eligibility, rolling 30-day CHF-denominated limits, payout contacts, and deposit bank details per currency. Verify outgoing amounts against limits.available before submitting.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v2/br":{"get":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetProfile","summary":"Get user banking profile","description":"Returns the user's banking profile: IBAN, holder name, address, card eligibility, rolling 30-day CHF-denominated limits, payout contacts, and deposit bank details per currency. Verify outgoing amounts against limits.available before submitting.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Get card info

Returns card metadata, limits, masked fields, and a short-lived cardToken (5 minute expiry) for rendering sensitive card details through UR's card display script. Do not store or log cardToken; use externalId for card management APIs.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v2/card":{"get":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetCardInfo","summary":"Get card info","description":"Returns card metadata, limits, masked fields, and a short-lived cardToken (5 minute expiry) for rendering sensitive card details through UR's card display script. Do not store or log cardToken; use externalId for card management APIs.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Create card

Applies for a new virtual card after KYC passes and card issuance conditions are met. Send an empty JSON body.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v2/card":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaCreateCard","summary":"Create card","description":"Applies for a new virtual card after KYC passes and card issuance conditions are met. Send an empty JSON body.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Set default transaction currency

Sets the default transaction currency for the user's card. Use the stable externalId returned by Get card info as cardExternalId.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v2/card-currency":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaSetCardCurrency","summary":"Set default transaction currency","description":"Sets the default transaction currency for the user's card. Use the stable externalId returned by Get card info as cardExternalId.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"cardExternalId":{"type":"string","description":"Stable card external ID from GET /api/v2/card (result.externalId)."},"currency":{"type":"string","description":"Default transaction currency, such as USD, EUR, or CHF."}},"required":["cardExternalId","currency"]}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Update card status

Freezes (status 0) or unfreezes (status 1) the user's card. cardTokenId is the card's own token, not the user's URID.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v2/card-status":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaUpdateCardStatus","summary":"Update card status","description":"Freezes (status 0) or unfreezes (status 1) the user's card. cardTokenId is the card's own token, not the user's URID.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"cardTokenId":{"type":"string","description":"The card's token (the card's specific ID, not the user's URID), retrieved from the cards interface."},"status":{"type":"integer","description":"Target status: 0 inactive (blocked), 1 active (unblocked)."}},"required":["cardTokenId","status"]}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Get transaction history

Returns the user's transaction history with date range, type, currency, direction, and amount filters plus cursor pagination. Timestamps are Unix seconds.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v2/transactions":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetTransactions","summary":"Get transaction history","description":"Returns the user's transaction history with date range, type, currency, direction, and amount filters plus cursor pagination. Timestamps are Unix seconds.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"pageSize":{"type":"integer","description":"Page size (default 50)."},"fromTimestamp":{"type":"integer","description":"Start timestamp (Unix seconds)."},"toTimestamp":{"type":"integer","description":"End timestamp (Unix seconds)."},"type":{"type":"string","description":"Single transaction type, e.g. P2P, FRX, CTU, CRD, CDP. Mutually exclusive with transactionTypes."},"transactionTypes":{"type":"array","items":{"type":"string"},"description":"Multiple transaction-type filter. Mutually exclusive with type."},"currencys":{"type":"array","items":{"type":"string"},"description":"Currency filter, e.g. EUR, USD, CHF."},"direction":{"type":"string","description":"IN, OUT, or ALL."},"minAmount":{"type":"string","description":"Minimum amount filter."},"maxAmount":{"type":"string","description":"Maximum amount filter."},"status":{"type":"string","description":"Transaction status filter."},"chainId":{"type":"string","description":"CAIP-2 chain ID, e.g. eip155:5000."},"tokenSymbol":{"type":"string","description":"Token symbol, e.g. USDC."},"cursorTimestamp":{"type":"integer","description":"Forward-pagination cursor timestamp."},"cursorId":{"type":"integer","description":"Forward-pagination cursor ID."},"id":{"type":"integer","description":"Query a single transaction by record ID. Mutually exclusive with txHash."},"txHash":{"type":"string","description":"Query a single transaction by hash. Mutually exclusive with id."}}}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Submit token permit

Submits an EIP-2612 permit signature so UR contracts can spend tokens from the user's wallet without an on-chain approve transaction. Used for card spending against the user's UR fiat balance.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v1/token-permit":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaSubmitTokenPermit","summary":"Submit token permit","description":"Submits an EIP-2612 permit signature so UR contracts can spend tokens from the user's wallet without an on-chain approve transaction. Used for card spending against the user's UR fiat balance.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"address":{"type":"string","description":"Contract address of the token being approved."},"amount":{"type":"string","description":"Amount to approve (decimal string)."},"permitAmount":{"type":"string","description":"Permit amount signed in the EIP-2612 permit (decimal string)."},"permitDeadline":{"type":"integer","description":"Unix seconds until which the permit is valid."},"permitV":{"type":"integer","description":"EIP-2612 signature component v."},"permitR":{"type":"string","description":"EIP-2612 signature component r (32-byte hex)."},"permitS":{"type":"string","description":"EIP-2612 signature component s (32-byte hex)."}},"required":["address","amount","permitAmount","permitDeadline","permitV","permitR","permitS"]}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Get supported chain config

Returns supported chains, token lists, contract addresses, and per-token Off-ramp and FX amount limits (minTopUpAmount, maxTopUpAmount, minFxAmount, maxFxAmount). Works logged in (adds balances and card eligibility) and logged out (config only). Read limits at request time; do not hardcode them.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[],"paths":{"/api/v3/config/chain-configs":{"get":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetChainConfigs","summary":"Get supported chain config","description":"Returns supported chains, token lists, contract addresses, and per-token Off-ramp and FX amount limits (minTopUpAmount, maxTopUpAmount, minFxAmount, maxFxAmount). Works logged in (adds balances and card eligibility) and logged out (config only). Read limits at request time; do not hardcode them.","parameters":[{"name":"tokenId","in":"header","required":false,"schema":{"type":"string"},"description":"The user's URID. Omit for a non-logged-in query (config only, no balances)."},{"name":"network","in":"header","required":false,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":false,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":false,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":false,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}},"components":{"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}}}
```


# Get off-ramp quote (EVM)

Returns the best aggregator quote for an EVM crypto deposit into fiat, including network and cross-chain fees. Quotes are cached for 60 seconds. Pass best.to, best.swapCalldata, and best.minUsdcAmount to the Off-ramp contract's depositTokenViaAggregator call.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[],"paths":{"/api/v1/partner/quote/deposit":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetOfframpQuote","summary":"Get off-ramp quote (EVM)","description":"Returns the best aggregator quote for an EVM crypto deposit into fiat, including network and cross-chain fees. Quotes are cached for 60 seconds. Pass best.to, best.swapCalldata, and best.minUsdcAmount to the Off-ramp contract's depositTokenViaAggregator call.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"chainId":{"type":"string","description":"Source chain ID (CAIP-2), e.g. eip155:1."},"userAddress":{"type":"string","description":"User wallet address used to build the route."},"fromToken":{"type":"string","description":"Source token address. Use the zero address for native tokens."},"toToken":{"type":"string","description":"Target fiat token contract address."},"amount":{"type":"string","description":"Deposit amount in the token's smallest unit."}},"required":["chainId","userAddress","fromToken","toToken","amount"]}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}},"components":{"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}}}
```


# Get off-ramp quote (Solana)

Returns execution parameters and a fiat-credit quote for a Solana USDC deposit, including an optional server-built unsigned VersionedTransaction. Partner-signed request on the partner API base URL. Validate the transaction structure before signing; production callers must pass network mainnet explicitly.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://openapi.ur.app","description":"Production (partner API)"},{"url":"https://uropenapi-qa.ur-inc.xyz","description":"Testnet (partner API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"DcEnvelope":{"type":"object","properties":{"code":{"type":"integer","description":"0 on success; non-zero indicates a business rejection.","format":"int64"},"message":{"type":"string","description":"Human-readable diagnostic; empty on success."},"data":{"description":"Business payload. Shape depends on the endpoint."}},"required":["code","message"],"description":"Standard response envelope for the partner API (openapi.ur.app)."}}},"paths":{"/v1/solana/deposit/quote":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetSolanaDepositQuote","summary":"Get off-ramp quote (Solana)","description":"Returns execution parameters and a fiat-credit quote for a Solana USDC deposit, including an optional server-built unsigned VersionedTransaction. Partner-signed request on the partner API base URL. Validate the transaction structure before signing; production callers must pass network mainnet explicitly.","parameters":[{"name":"X-Api-Signature","in":"header","required":true,"schema":{"type":"string"},"description":"Partner's EIP-191 signature (0x-prefixed hex)."},{"name":"X-Api-Deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Unix seconds when the request signature expires (within 5 minutes)."},{"name":"X-Api-PublicKey","in":"header","required":false,"schema":{"type":"string"},"description":"Partner's signer Ethereum address."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"urId":{"type":"integer","description":"User URID.","format":"int64"},"solanaAddress":{"type":"string","description":"User's Solana address (Base58); must match the transaction signing key."},"usdcAmount":{"type":"string","description":"Planned USDC input in smallest units (6 decimals). Minimum 5 USDC (\"5000000\")."},"outputCurrency":{"type":"string","description":"Target fiat currency code, e.g. USD."},"minUsdcAmount":{"type":"string","description":"Client-side slippage floor in smallest units. Omit or pass \"0\" for USDC-only deposits; the on-chain check runs after fee deduction."},"network":{"type":"string","description":"Solana cluster: mainnet for production, devnet for testing. Defaults to devnet when omitted."},"computeUnitPrice":{"type":"integer","description":"Solana priority fee in micro-lamports per compute unit. Server default 1000.","format":"int64"}},"required":["urId","solanaAddress","usdcAmount","outputCurrency"]}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DcEnvelope"}}}}}}}}}
```


# Get on-ramp limit

On-ramp is not yet generally available; this endpoint is documented for preview only. Returns on-ramp eligibility signals and per-currency amount caps. Evaluate regionLocked, usdcDepegged, livenessLocked, maxAmounts, and minAmounts before starting the flow.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v1/onramp-limit":{"get":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetOnrampLimit","summary":"Get on-ramp limit","description":"On-ramp is not yet generally available; this endpoint is documented for preview only. Returns on-ramp eligibility signals and per-currency amount caps. Evaluate regionLocked, usdcDepegged, livenessLocked, maxAmounts, and minAmounts before starting the flow.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Get on-ramp quote

On-ramp is not yet generally available; this endpoint is documented for preview only. Returns a quote for scene onramp (fiat token to destination token) or scene swap\_retry (retry a failed destination swap). When needLiveness is true, the user must pass liveness before submitting.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v1/quote/onramp":{"post":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetOnrampQuote","summary":"Get on-ramp quote","description":"On-ramp is not yet generally available; this endpoint is documented for preview only. Returns a quote for scene onramp (fiat token to destination token) or scene swap_retry (retry a failed destination swap). When needLiveness is true, the user must pass liveness before submitting.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scene":{"type":"string","description":"onramp for the main flow; swap_retry for retrying a failed swap leg.","enum":["onramp","swap_retry"]},"srcChainId":{"type":"string","description":"Source chain ID (CAIP-2)."},"dstChainId":{"type":"string","description":"Destination chain ID (CAIP-2)."},"fromToken":{"type":"string","description":"Fiat token contract address."},"toToken":{"type":"string","description":"Destination token address."},"amount":{"type":"string","description":"Input amount in smallest units (e.g. \"10000\" is 100 USD)."},"slippageBps":{"type":"integer","description":"Slippage tolerance in basis points."}},"required":["scene","srcChainId","dstChainId","fromToken","toToken","amount"]}}}},"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```


# Get liveness token

Creates a Sumsub liveness access token for the on-ramp compliance flow. Call only when the on-ramp quote returns needLiveness true.

```json
{"openapi":"3.1.0","info":{"title":"UR API","version":"1.0.0"},"tags":[{"name":"External Wallet Access Mode","description":"APIs signed by the user's own wallet key. Base URL https://api.ur.app."}],"servers":[{"url":"https://api.ur.app","description":"Production (user API)"},{"url":"https://urapi3-qa.ur-inc.xyz","description":"Testnet (user API)"}],"security":[{"partnerAuth":[]},{"userWalletAuth":[]}],"components":{"securitySchemes":{"partnerAuth":{"type":"apiKey","in":"header","name":"X-Api-Signature","description":"Partner Auth: EIP-191 signature by the partner's registered backend key, with X-Api-Deadline and optional X-Api-PublicKey headers. See the Signature and verify guide."},"userWalletAuth":{"type":"apiKey","in":"header","name":"sign","description":"User wallet auth (External Wallet Access Mode): EIP-191 signature by the user's wallet key, with tokenId, network, hash, and deadline headers. See the Signature and verify guide."}},"schemas":{"EwaEnvelope":{"type":"object","properties":{"retCode":{"type":"integer","description":"0 on success; non-zero indicates failure.","format":"int64"},"retMsg":{"type":"string","description":"Human-readable message; error details when retCode is non-zero."},"result":{"description":"Business payload. Shape depends on the endpoint; some endpoints return it as a JSON-encoded string."},"timeNow":{"type":"integer","description":"Server timestamp in milliseconds.","format":"int64"}},"required":["retCode","retMsg"],"description":"Standard response envelope for the user API (api.ur.app)."}}},"paths":{"/api/v2/get-liveness-token":{"get":{"tags":["External Wallet Access Mode"],"operationId":"ewaGetLivenessToken","summary":"Get liveness token","description":"Creates a Sumsub liveness access token for the on-ramp compliance flow. Call only when the on-ramp quote returns needLiveness true.","parameters":[{"name":"tokenId","in":"header","required":true,"schema":{"type":"string"},"description":"The user's URID (NFT token id)."},{"name":"network","in":"header","required":true,"schema":{"type":"string"},"description":"Network identifier: 5000 for mainnet, 5003 for testnet."},{"name":"sign","in":"header","required":true,"schema":{"type":"string"},"description":"Signature generated by the user's wallet key (EIP-191)."},{"name":"hash","in":"header","required":true,"schema":{"type":"string"},"description":"SHA3/Keccak256 hash of the original request payload."},{"name":"deadline","in":"header","required":true,"schema":{"type":"string"},"description":"Signature expiry: server timestamp plus a validity window (max 20 minutes)."}],"responses":{"200":{"description":"Response envelope. For the user API, retCode 0 means success; for the partner API, code 0 means success. Business rejections return HTTP 200 with a non-zero code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EwaEnvelope"}}}}}}}}}
```




---

[Next Page](/llms-full.txt/1)

