> 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/api-reference/account/managed-custody-mode.md).

# 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.
* Routine banking actions (FX, Pay-in, Payout, On-ramp (coming soon), 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 financial 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** (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  |
| **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.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](/api-reference/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

{% 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.md). 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.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. 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). Note: `fma.kyc.retry_required` is **not** emitted today (planned only).

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 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: **PLANNED, not yet emitted.** `fma.kyc.retry_required` is a reserved event name with no dispatch in production today; the payload below is the designed contract. Do not build a hard dependency on receiving it. The live handoff-verdict webhook for this flow is `fma.kyc.reuse_check.result` (see [Webhooks](https://docs.ur.app/developer-resources/webhook)).

```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/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.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; 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.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. |                                    |

**For Tempo chain**, use `depositWithFeeTo` on the Tempo Off-ramp contract. Contract addresses per chain: see [Deposit (off-ramp)](/api-reference/smart-contracts.md#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

> **FX** converts one tokenized fiat balance into another tokenized fiat balance, both held inside the user's 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.

***

## 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.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"
}
```

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.md#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.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.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.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](/api-reference/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`, `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.md#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.md)
* [Webhook Reference](https://docs.ur.app/developer-resources/webhook)
* [API Reference: Card Mode: Crypto Backed](/api-reference/cards/crypto-backed-card.md): 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.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, and the optional `goal` query parameter:

```
GET https://docs.ur.app/api-reference/account/managed-custody-mode.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
