> For the complete documentation index, see [llms.txt](https://docs.ur.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ur.app/developer-resources/api-reference-managed-custody-mode.md).

# API reference: 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](/getting-started/integration-guide.md#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**.
* The Partner backend accesses that account **entirely through REST APIs**, signed with the Partner's signer key.
* Routine banking actions (FX, Pay-in, Payout, On-ramp, Off-ramp, Card) are submitted by the Partner's backend; UR validates, runs compliance / risk / limit checks, and executes the 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 banking infrastructure underneath.

### 1.2 Operation Layer Map

Every Core Banking endpoint maps to one of UR's six core operations. This table is the canonical anchor for the rest of this document.

| Operation    | Direction                                  | Endpoint family                                                                                                                                         | Settlement                                |
| ------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| **On-ramp**  | User fiat → crypto (out to a target chain) | [§10](#id-10-on-ramp)                                                                                                                                   | Async via webhook                         |
| **Off-ramp** | Crypto (from external chain) → 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  |
| **Pay-in**   | External bank → user IBAN                  | Covered in [Deposits](/money-movement/deposits.md)                                                                                                      | 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](/developer-resources/api-reference-card-mode-crypto-backed.md)) | 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](/developer-resources/signature-and-verify.md).

### 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, 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. Replays with the same `reqId` return the original outcome instead of re-executing.
* 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, 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

Use this section when your platform verifies the user through your own Sumsub tenant. During onboarding, your platform shares the completed Sumsub applicant with UR through Sumsub's share-token reuse mechanism. UR reruns the required checks against UR's Sumsub 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](/developer-resources/api-reference-managed-custody-sdk-kyc.md). 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.
* 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](/getting-started/integration-guide.md#kyc-data-sharing-disclosure).
* Your backend can generate a Sumsub share token scoped to UR's Sumsub `clientId` after the applicant is approved.
* Your backend can submit the share token through your dedicated integration channel.
* Your backend can persist `externalUserId`, `sessionId`, `urId`, and `evmAddress` for each user.
* Your backend can receive onboarding webhooks from UR, especially `fma.kyc.retry_required`.

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 shared the approved applicant with UR through Sumsub reuse. UR validates the reused KYC snapshot, records the required attestation on UR's Sumsub level, and activates the UR Account asynchronously.

```mermaid
sequenceDiagram
    participant User as User
    participant Partner as Partner backend
    participant UR as UR OpenAPI
    participant SS as Sumsub reuse
    participant Bank as Banking partner

    User->>Partner: Completes KYC in partner app
    Partner->>SS: Create share token for UR
    Partner->>UR: POST /api/fma/v1/create-account
    UR-->>Partner: sessionId, urId, evmAddress
    loop Poll every 30 seconds to 2 minutes
        Partner->>UR: POST /api/fma/v1/kyc/check
        UR->>SS: Reuse applicant at UR level
        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`, the user's email, and the completed Sumsub `applicantId`. UR creates or reuses the user's URID, provisions the UR-managed wallet, and returns `sessionId`, `urId`, and `evmAddress`.
{% endstep %}

{% step %}

### Poll KYC completeness

Call `POST /api/fma/v1/kyc/check` with the returned `sessionId`. UR validates the reused Sumsub applicant against UR's Sumsub level and returns `complete=true` when the session can proceed.
{% 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
{
  "applicantId": "sumsub-applicant-123",
  "email": "user@example.com"
}
```

Request fields:

| Field         | Required | Description                                                                                                              |
| ------------- | -------- | ------------------------------------------------------------------------------------------------------------------------ |
| `applicantId` | Yes      | The completed Sumsub applicant ID in your tenant. UR uses this value to match the applicant shared through Sumsub reuse. |
| `email`       | Yes      | The user's email address.                                                                                                |

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

Validate the user's reused Sumsub applicant and check whether the session can proceed to Form A.

| 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"
    ]
  }
}
```

Poll this endpoint every **30 seconds to 2 minutes** while the user is completing KYC or while UR is waiting for the Sumsub reuse result. Stop polling after `/kyc/submit` succeeds, or when the session reaches a terminal state.

Error handling:

| Code    | Meaning                                                           | Partner action                                                    |
| ------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- |
| `20004` | The KYC snapshot is incomplete.                                   | Ask the user to finish the missing Sumsub steps, then poll again. |
| `40001` | Sumsub is temporarily unavailable.                                | Back off and retry.                                               |
| `40002` | UR cannot match the `applicantId` to a reusable Sumsub applicant. | Check the `applicantId` and share-token setup for the user.       |

### 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 and wait for UR guidance or a retry webhook.           |

### 3.11 Onboarding webhooks

UR sends onboarding webhooks to the webhook URL registered for your integration. 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:

```json
{
  "event": "fma.kyc.retry_required",
  "timestamp": 1704234567,
  "data": {
    "urId": 7123456789,
    "externalUserId": "partner-user-0001",
    "retryLevel": "ResetSumsub",
    "retryReason": "document_expired",
    "requiredFields": ["registerRequest.id.NFC"],
    "requiredActions": ["recollect_sumsub", "resign_form_a", "resubmit_kyc"],
    "retrySessionId": "new-session-id",
    "retryOfSessionId": "old-session-id"
  }
}
```

When you receive `fma.kyc.retry_required`, store `retrySessionId` and run the onboarding flow again from `/kyc/check` against the retry session. Do not call `/create-account` for a retry session.

***

## 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/developer-resources/pages/yEG7tyYvgaB42helZKpJ#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/developer-resources/pages/yEG7tyYvgaB42helZKpJ#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](/developer-resources/api-reference-card-mode-crypto-backed.md) 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.
* A single 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 both automatically when the user reaches `Live`; there is no application step or prerequisite pay-in. The USD deposit account 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](/money-movement/deposits.md#usd-deposits-from-a-third-party-account).

***

## 6. Balance

### 6.1 Get User Balance

Fetch the user's fiat and crypto balances held inside the user's UR account.

| 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" }
    ],
    "cryptoItems": [
      { "coin": "USDC", "amount": "1000.50" }
    ]
  }
}
```

* `fiatItems` enumerates the user's tokenized fiat balances on Mantle (EUR24, CHF24, USD24, etc.).
* `cryptoItems` enumerates any crypto held inside the user's UR-managed wallet, if Partner has enabled crypto-holding for that user.

***

## 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/developer-resources/pages/yEG7tyYvgaB42helZKpJ#id-3.1.9-get-supported-chain-config)
* **Target fiat currencies:** USD, EUR, CHF, SGD, JPY, HKD

### Flow

```mermaid
sequenceDiagram
    participant Partner as Partner Frontend
    participant UR as UR OpenAPI
    participant W as User Crypto Wallet<br/>(UR-managed or external)
    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 user (via the Partner Frontend) signs and submits the Off-ramp contract call from whichever wallet holds the source crypto. 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/developer-resources/pages/yEG7tyYvgaB42helZKpJ#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"
  }
}
```

Contract execution notes:

* Pass `best.to`, `best.swapCalldata`, and `best.minUsdcAmount` to the UR Off-ramp contract **exactly as returned**.
* The signing Crypto Wallet (UR-managed or external; 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**, but the **Crypto Wallet could be the same as the UR-managed account or an external wallet**, depending on the Partner's choice. The Off-ramp contract is agnostic to the source — it uses the `_targetAccount` parameter to know which UR Account receives the resulting fiat.

**Contract**: `depositTokenViaAggregatorToAccount` on the Off-ramp contract. Contract addresses per chain: see [Deposit (Off-ramp)](/developer-resources/smart-contracts.md#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. |                                    |

If you require Partner-side API submission for Off-ramp instead of on-chain user signing, please contact the UR team.

***

## 8. FX

> **FX** converts one tokenized fiat balance into another tokenized fiat balance, both held inside the user's UR account.

### 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.

***

## 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

> **On-ramp** converts the user's tokenized fiat balance into crypto on a target chain.

**Currently supported:**

* **Destination chains and tokens:** see cryptos with `aggregatorSupported` value in the response of [Supported Chains & Tokens](https://docs.ur.app/developer-resources/pages/yEG7tyYvgaB42helZKpJ#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"
    },
    "usdcDepegged": false,
    "regionLocked": false
  }
}
```

Block the flow if `regionLocked`, `usdcDepegged`, or `livenessLocked` is `true`, or if the requested amount exceeds `maxAmounts[currency]`.

### 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/developer-resources/pages/yEG7tyYvgaB42helZKpJ#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_TARGET_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 optional and is only supported for cross-chain On-ramp (`chainId != dstChainId`).
* If `withdrawAddress` is omitted, UR sends the destination-chain crypto to the user's UR EVM address.
* Do not send `withdrawAddress` for same-chain On-ramp (`chainId == dstChainId`); the request will be rejected.
* 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**](/developer-resources/api-reference-card-mode-crypto-backed.md).

### 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",
  "cardExternalId": "1758893252"
}
```

Request fields:

| Field            | Type   | Required | Description                                                                      |
| ---------------- | ------ | -------- | -------------------------------------------------------------------------------- |
| `currency`       | string | Yes      | Target default transaction currency, such as `USD`, `EUR`, or `CHF`.             |
| `cardExternalId` | string | Yes      | Stable card external ID returned by `GET /api/fma/v1/card` as `data.externalId`. |

Response example:

```json
{ "code": 0, "message": "" }
```

Notes:

* The Partner must pass `cardExternalId` for the target card.
* 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](/developer-resources/api-reference-card-mode-crypto-backed.md#id-4-card-authorization-callback-ur-greater-than-partner).

### 11.4 Card Authorization

Card authorization behavior is **Card-Mode-specific** and is not covered in this document. See [API Reference: Card Mode — Crypto Backed](/developer-resources/api-reference-card-mode-crypto-backed.md#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.5 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",
        "detailsJson": "{\"merchant\":\"Merchant ABC\"}"
      }
    ],
    "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. Do not send together with `txHash`. |
| `txHash`              | string    | Exact lookup by transaction hash. Do not send together with `id`.               |

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, or `id` for UR's internal transaction ID.

| 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
}
```

Response:

* The response uses the same `data.items[]` transaction structure as [§12.1](#id-12.1-fetch-transaction-history).
* Send either `id` or `txHash`, never both.
* If no transaction matches the lookup key, `data.items[]` is empty.

***

## 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](/developer-resources/api-reference-card-mode-crypto-backed.md#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](/developer-resources/signature-and-verify.md) 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/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` or `id` 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](/developer-resources/api-reference-card-mode-crypto-backed.md#id-7-implementation-checklist).

***

## 15. Reference Docs

* [Integration Guide](/getting-started/integration-guide.md) — Account Mode, Card Mode, KYC Mode decisions.
* [Core Banking Overview](/money-movement/core-banking-overview.md)
* [Signature and Verification](/developer-resources/signature-and-verify.md)
* [Webhook Reference](/developer-resources/webhook.md)
* [API Reference: Card Mode — Crypto Backed](/developer-resources/api-reference-card-mode-crypto-backed.md) — the Partner-side integration surface for Card Mode: Crypto Backed (authorization callback, Prefund Account, card-related webhooks).
* [API Reference: External Wallet Access Mode](/developer-resources/api-reference-external-wallet-access-mode.md) — the other Account Mode.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.ur.app/developer-resources/api-reference-managed-custody-mode.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
