# Fiat instructions

A *fiat instruction* binds a unique **memo** to one side of a fiat movement, so your systems can programmatically correlate a USD fiat payment to the RLUSD on-chain transaction it belongs to. There are two kinds, and you only ever create one of them:

- **Buy RLUSD (USD payment in → RLUSD mint)** — the **buy instruction** API. You `PUT` an instruction declaring the incoming fiat payment and the wallet to mint into, and it returns the **memo** to put in the payment's reference field.
- **Redemptions (RLUSD burn → USD payment out)** — nothing for you to create. Ripple generates the instruction and its memo when your redemption is registered, puts that memo in the reference field of the fiat payment it sends to your bank, and surfaces it on your existing `REDEMPTION` transaction (Transaction API / `STABLECOIN_TRANSACTION` webhook you already consume). **No new endpoint to integrate.**


Both kinds are read back through the same endpoints and share one object shape, which is what makes a single integration cover both directions. Authentication, base URL, and scopes are unchanged from the rest of the Ripple Mint API.

Enablement
The Fiat Instructions API and the redemption-memo flow are enabled **per customer** by Ripple. Until your account is enabled, the endpoints are unavailable and your `REDEMPTION` transactions carry the **wallet ripId** as the memo rather than a Ripple-generated `RL...` memo. Ask your Ripple liaison to enable it.

## At a glance

| Area | Behavior |
|  --- | --- |
| Resource path | `/v1/stablecoin/fiat-instructions` |
| Memo format | `RL` + 14 Crockford base32 characters, no dash (16 characters total) |
| `PUT` semantics | Idempotent on `id` — a retry returns the existing instruction and its original memo |
| Instruction status | `UNUSED` → `USED` / `CANCELED` |
| Expiry | **None.** An `UNUSED` instruction and its memo stay valid indefinitely |
| Reusability | Single-use — a memo is claimed by exactly one fiat payment, then terminal |
| Transaction / webhook fields | `fiatInstructionId` and `memo`, on the `FIAT` leg only |
| Redemption memo | Backed by a Ripple-created `FIAT`-destination instruction, readable via the same API |
| Bank details | `bankDetails` on the instruction — where to wire the USD for a buy. Optional; see [Bank details](#bank-details) |


## Concepts

**Fiat instruction** — a small record that binds a globally unique **memo** to one side of a fiat movement. There are two kinds, distinguished by `destination.type`:

| Kind | `destination.type` | Created by | Purpose |
|  --- | --- | --- | --- |
| **Buy instruction** | `CRYPTO` | **You**, via `PUT /v1/stablecoin/fiat-instructions/{id}` | Declares an inbound USD fiat payment and the wallet the minted RLUSD should land in |
| **Redemption instruction** | `FIAT` | **Ripple**, automatically, when your redemption is registered | Carries the memo Ripple puts in the outbound USD fiat payment's reference field |


Both kinds are returned by the same read endpoints and use the same object shape, so a single integration reads both.

**Memo** — a 16-character routing token (`RL` + 14 characters) that travels in the fiat payment's reference field. It is the single string that ties a bank payment to an RLUSD transaction. Memos are globally unique across buys and redemptions, so one regex extracts memos from your bank feed in either direction.

**Single-use** — a memo is claimed exactly once. For a buy, the claim happens when the matching fiat payment is processed; for a redemption, when the memo is bound to its outbound payment. After the claim the instruction is `USED` and terminal — a second fiat payment carrying the same memo will never mint again.

**No expiry** — instructions and their memos never time out. An `UNUSED` instruction stays claimable indefinitely: there is no TTL and no sweep job. The only ways an instruction leaves `UNUSED` are in the lifecycle below: its payment claims it (`USED`), Ripple cancels the fiat payment behind it (`CANCELED`), or you delete it. Do **not** build re-issue-on-expiry logic, and do not re-`PUT` a new instruction for a payment you have already released.

## Fiat instruction object

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "memo": "RL9CFXQSCMSP4K7T",
  "currency": "USD",
  "destination": {
    "type": "CRYPTO",
    "walletRipId": "RIP0000001"
  },
  "status": "UNUSED",
  "createdAt": "2026-07-28T18:22:01Z",
  "bankDetails": {
    "bankName": "ABC Bank",
    "accountName": "Example Beneficiary Ltd.",
    "accountNumber": "0000001234",
    "swiftBicCode": "ABCBUS33XXX",
    "routingNumber": "0000",
    "reference": "RLUSD",
    "bankAddress": {
      "street": "1 Example Street",
      "city": "Example City",
      "state": "Example State",
      "zipCode": "00000",
      "country": "Example Country"
    }
  }
}
```

| Field | Type | Nullable | Description |
|  --- | --- | --- | --- |
| `id` | string (UUID) | No | Instruction identifier. **You** supply it for buy instructions (in the `PUT` path); Ripple generates it for redemption instructions |
| `memo` | string | No | The routing token. **Include verbatim in your fiat payment's reference field** (buys). See [Memo format](#memo-format) |
| `currency` | string | Yes | ISO-4217 fiat currency. Always present on buy instructions. May be absent on a redemption instruction when the originating fiat payment carries no fiat currency |
| `destination` | object | No | See [Instruction destination](#instruction-destination). `CRYPTO` = buy instruction, `FIAT` = redemption instruction |
| `status` | string | No | `UNUSED`, `USED`, or `CANCELED`. See [Instruction status lifecycle](#instruction-status-lifecycle) |
| `createdAt` | string (ISO 8601) | No | When the instruction was created |
| `bankDetails` | object | Yes | The Ripple bank account to wire your USD to for a buy. Absent when your account has no linked bank details. See [Bank details](#bank-details) |


There is no customer-supplied free-form reference field. Correlate using `id` (which you choose for buys) and `memo`.

### Instruction destination

| Field | Type | Nullable | Description |
|  --- | --- | --- | --- |
| `type` | string | No | `CRYPTO` (buy — payout to a wallet) or `FIAT` (redemption — payout to your bank account) |
| `walletRipId` | string | Yes | Wallet ripId in `RIPxxxxxxx` format. **Required** when `type` is `CRYPTO`; always absent when `type` is `FIAT` |


### Bank details

Where to send the USD for a **buy**. Resolved from the default bank account registered against your stablecoin product, so it is the same across your instructions rather than unique to one. Quote the instruction's [`memo`](#memo-format) on the transfer — without it the payment cannot be attributed and the mint will be delayed.

| Field | Type | Nullable | Description |
|  --- | --- | --- | --- |
| `bankName` | string | No | Name of the bank holding the account. May be an empty string |
| `accountName` | string | No | Name the account is held under — the Ripple legal entity you are paying. May be an empty string |
| `accountNumber` | string | Yes | Account number to wire to |
| `iban` | string | Yes | IBAN to wire to, for accounts identified that way |
| `swiftBicCode` | string | Yes | SWIFT/BIC code of the bank |
| `routingNumber` | string | Yes | Routing number of the bank |
| `reference` | string | Yes | Reference Ripple wants quoted on transfers to this account. **Distinct from `memo`**, which is what actually attributes the payment — send both |
| `bankAddress` | object | Yes | Postal address of the account (`street`, `city`, `state`, `zipCode`, `country`) |


At least one of `accountNumber` and `iban` is always present. The same object is served on its own by [`GET /v1/stablecoin/fiat-bank-details`](#get-bank-details), for callers that want the wire destination without minting or listing an instruction.

### Memo format

`RL` followed by **14** [Crockford base32](https://www.crockford.com/base32.html) characters — digits `0`–`9` plus uppercase `A`–`Z` excluding `I`, `L`, `O`, and `U` to avoid transcription ambiguity. **16 characters total, no dash or separator.**

```
Regex:   RL[0-9A-HJKMNP-TV-Z]{14}
Example: RL9CFXQSCMSP4K7T
```

16 characters fits SWIFT MT103 field 20 — the most restrictive reference field across the rails we use — so the same memo can be carried unchanged on any fiat payment.

Ripple locates the memo by **substring search**, so a bank that wraps it in surrounding free text (`PAYMENT REF RL9CFXQSCMSP4K7T THANKS`) still matches. Do not insert spaces, dashes, or line breaks *inside* the token, and do not change its case.

### Instruction status lifecycle

```
                     ┌──────► USED      (claimed by its payment — terminal)
   UNUSED ───────────┤
 (on create)         └──────► CANCELED  (originating fiat payment was cancelled — terminal)

   (no time-based transition: UNUSED never expires)
```

| Status | Meaning | Set by |
|  --- | --- | --- |
| `UNUSED` | Created, memo not yet consumed. The only non-terminal status, and it does not expire | On create |
| `USED` | The memo has been claimed — for a buy, an arriving fiat payment matched it and minting is under way; for a redemption, the memo has been bound to the outbound payment. Terminal | Ripple, when the payment claims it |
| `CANCELED` | The fiat payment the instruction belonged to was cancelled by Ripple (e.g. a reversed deposit). Terminal | Ripple |


`UNUSED` is the only status that can transition, and only on one of the events above — never on the passage of time. Any attempt to move an instruction out of a terminal status is rejected with `409 FIAT_INSTRUCTION_INVALID_STATUS_TRANSITION`.

Note
The instruction `CANCELED` status is unrelated to the transaction [`CANCELED` status](/products/stablecoin/api/transactions#transaction-statuses) introduced in v1.3, though both stem from a fiat receipt being reversed.

## Fiat instruction endpoints

| Method | Path | Scope | Description |
|  --- | --- | --- | --- |
| `PUT` | `/v1/stablecoin/fiat-instructions/{id}` | `rlusd_customers:write` | Create a buy instruction (client-supplied id). Returns the memo |
| `GET` | `/v1/stablecoin/fiat-instructions/{id}` | `rlusd_customers:read` | Get one instruction (buy or redemption) |
| `GET` | `/v1/stablecoin/fiat-instructions` | `rlusd_customers:read` | List your instructions (paginated) |
| `DELETE` | `/v1/stablecoin/fiat-instructions/{id}` | `rlusd_customers:write` | Delete an unused buy instruction |
| `GET` | `/v1/stablecoin/fiat-bank-details` | `rlusd_customers:read` | Get Ripple's bank details on their own, without an instruction |


Redemption instructions are **read-only** to you: they are created by Ripple and cannot be created, modified, or deleted through this API.

### Create a buy instruction

```
PUT /v1/stablecoin/fiat-instructions/{id}
```

You generate the UUID for `{id}` client-side and use it as the resource identifier.

Idempotent
Retrying with the same `id` and the same body returns the instruction that already exists, with its original `memo` — no second memo is minted, so a network retry is always safe. Reusing an `id` with a *different* body is rejected with `409 FIAT_INSTRUCTION_ALREADY_EXISTS`: the `id` is the instruction's identity, so a new instruction needs a new `id`.

#### Path parameters

| Parameter | Type | Description |
|  --- | --- | --- |
| `id` | string (UUID) | Client-generated UUID identifying the instruction |


#### Request body

```json
{
  "currency": "USD",
  "destination": {
    "type": "CRYPTO",
    "walletRipId": "RIP0000001"
  }
}
```

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| `currency` | string | Yes | ISO-4217 code of the fiat you will send. Today: `USD` |
| `destination.type` | string | Yes | Must be `CRYPTO`. `FIAT` is reserved for Ripple-created redemption instructions |
| `destination.walletRipId` | string | Yes | Target wallet in `RIPxxxxxxx` format. RLUSD is minted here when the fiat payment arrives. Must be one of your wallets |


#### Example request

```http
PUT /v1/stablecoin/fiat-instructions/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "currency": "USD",
  "destination": {
    "type": "CRYPTO",
    "walletRipId": "RIP0000001"
  }
}
```

#### Response — `200 OK`

The response is a [Fiat instruction object](#fiat-instruction-object) with its minted `memo` and `status: "UNUSED"`.

Important
Put the `memo` value **verbatim** into your fiat payment's reference field (SWIFT MT103 field 20/70, Fedwire OBI, etc.). This is how Ripple correlates the payment back to this instruction and knows which wallet to mint into.

**Do not also put a `RIPxxxxxxx` wallet ripId in the payment's free-text fields.** A wallet ripId token takes precedence over a memo during matching: the payment would be credited to that wallet and your instruction would stay `UNUSED`, so no `fiatInstructionId`/`memo` would appear on the resulting transaction. Send the memo alone.

#### Errors

| Status | Code | When |
|  --- | --- | --- |
| `400` | `INVALID_REQUEST_BODY` | Missing/unknown fields, `destination.type` other than `CRYPTO`, or `CRYPTO` without a `walletRipId` |
| `403` | `FORBIDDEN` | Ripple holds no RLUSD customer record for your tenant |
| `409` | `FIAT_INSTRUCTION_ALREADY_EXISTS` | An instruction with this `id` already exists with a different body |


### Get an instruction

```
GET /v1/stablecoin/fiat-instructions/{id}
```

**Response:** a single [Fiat instruction object](#fiat-instruction-object). Works for both buy and redemption instructions belonging to your account.

Returns `404 FIAT_INSTRUCTION_NOT_FOUND` if the id is unknown, belongs to another customer, or has been deleted, or `403 FORBIDDEN` if Ripple holds no RLUSD customer record for your tenant.

### List instructions

```
GET /v1/stablecoin/fiat-instructions
```

#### Query parameters

| Parameter | Type | Required | Description |
|  --- | --- | --- | --- |
| `page` | integer | No | Page number, 0-indexed (default: `0`) |
| `size` | integer | No | Page size (default: `20`) |
| `sort` | string | No | Sort field and direction (default: `createdAt,desc`) |


The list contains both buy (`CRYPTO`) and redemption (`FIAT`) instructions. Deleted instructions are excluded. A known customer with no instructions gets an empty `200`; a tenant with no RLUSD customer record gets `403 FORBIDDEN`.

### Delete a buy instruction

```
DELETE /v1/stablecoin/fiat-instructions/{id}
```

Retires an instruction you no longer intend to use, so a fiat payment arriving later with its memo can never mint against it. Because instructions never expire, `DELETE` is the **only** way to retire an unused one.

**Response:** `200 OK`, empty body.

| Instruction state | Result |
|  --- | --- |
| `UNUSED` buy instruction | Deleted. Subsequent `GET`/`DELETE` on the id return `404 FIAT_INSTRUCTION_NOT_FOUND` |
| `USED` or `CANCELED` | `409 FIAT_INSTRUCTION_INVALID_STATUS_TRANSITION` — a claimed memo cannot be retired |
| Redemption (`FIAT`) instruction | `404 FIAT_INSTRUCTION_NOT_FOUND` — the endpoint only addresses your `CRYPTO` instructions |
| Unknown id, or another customer's | `404 FIAT_INSTRUCTION_NOT_FOUND` |


Deletion is permanent and there is no un-delete: create a new instruction with a new `id` (you will get a new memo). Transactions already produced from the instruction are unaffected and keep their `fiatInstructionId` and `memo`.

### Get bank details

```
GET /v1/stablecoin/fiat-bank-details
```

Returns the same [`bankDetails`](#bank-details) object embedded in every fiat instruction, on its own — for callers that want the wire destination without creating or listing an instruction.

#### Response — `200 OK`

```json
{
  "bankName": "ABC Bank",
  "accountName": "Example Beneficiary Ltd.",
  "accountNumber": "0000001234",
  "swiftBicCode": "ABCBUS33XXX",
  "reference": "RLUSD",
  "bankAddress": {
    "street": "1 Example Street",
    "city": "Example City",
    "zipCode": "00000",
    "country": "Example Country"
  }
}
```

**This endpoint does not return the memo.** A memo is per-payment and comes only from a fiat instruction; the bank details alone are not enough to have a payment attributed. Create an instruction for every payment you intend to send.

| Condition | Result |
|  --- | --- |
| Bank resolved | `200 OK` with the object above |
| Ripple holds no RLUSD customer record for your tenant | `403 FORBIDDEN` |
| No active default bank account, no link to a Ripple receiving bank, or the linked bank cannot be paid | `404 STABLECOIN_BANK_DETAILS_NOT_FOUND` |
| The details could not be retrieved | `502 UPSTREAM_UNAVAILABLE` — safe to retry |


Unlike the embedded `bankDetails` field — which is simply absent on a `404` or `502` so the instruction and its memo still come back — this endpoint keeps its failures apart, so `404` and `502` are distinguishable by status alone.

## Memos on transactions

`fiatInstructionId` and `memo` are added to the [Transaction endpoint](/products/stablecoin/api/transactions#transaction-endpoint) object (`source` / `destination`) returned by the Transaction API. They appear on the `FIAT` leg only. See [Transaction endpoint](/products/stablecoin/api/transactions#transaction-endpoint) for the field definitions, presence matrix, and contract guarantees.

**Example — `ISSUANCE` from a memo-matched fiat payment:**

```json
{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "type": "ISSUANCE",
  "status": "COMPLETED",
  "amount": "10000.00",
  "token": "RLUSD",
  "source": {
    "type": "FIAT",
    "fiatInstructionId": "550e8400-e29b-41d4-a716-446655440000",
    "memo": "RL9CFXQSCMSP4K7T"
  },
  "destination": {
    "type": "CRYPTO",
    "walletRipId": "RIP0000001",
    "address": "rN7M2B4y3kuoEaQfYMJDPKgEbVh4K8xm3p",
    "chain": "XRPL",
    "transactionHash": "4F2A8B1C9D3E7F6A0B5C4D8E2F1A9B3C7D6E0F5A4B8C2D1E9F3A7B6C0D5E8F01"
  },
  "createdAt": "2026-07-28T18:40:00Z",
  "updatedAt": "2026-07-28T18:45:12Z"
}
```

**Example — `REDEMPTION` with its system-generated memo:**

```json
{
  "id": "8d0f7780-8536-51ef-a55c-f18fd2090be8",
  "type": "REDEMPTION",
  "status": "PROCESSING",
  "amount": "25000.00",
  "token": "RLUSD",
  "source": {
    "type": "CRYPTO",
    "walletRipId": "RIP0000001",
    "address": "0x1234abcd5678ef90abcdef1234567890abcdef12",
    "chain": "ETH",
    "transactionHash": "0x4f2a8b1c9d3e7f6a0b5c4d8e2f1a9b3c7d6e0f5a4b8c2d1e9f3a7b6c0d5e8f01"
  },
  "destination": {
    "type": "FIAT",
    "fiatInstructionId": "7f1c2a90-8e4b-4d3a-9c22-b6a0d1e5f742",
    "memo": "RL7T2M9PVQR4XN3B"
  },
  "createdAt": "2026-07-28T14:24:55Z",
  "updatedAt": "2026-07-28T14:25:10Z"
}
```

## How memos work

Memos are only useful if exactly one party mints against them and nobody can guess or reuse them. Ripple therefore mints every memo — for buys and redemptions alike — and each memo is consumed by exactly one fiat movement:

- **Always Ripple-generated.** You never choose or supply a memo. Buy memos are minted when you create the instruction; redemption memos are minted when Ripple registers your outbound payment.
- **Globally unique.** Memos are unique across every customer and both directions, so a memo you find in your bank feed maps to at most one RLUSD transaction.
- **Unguessable.** `RL` plus 14 Crockford base32 characters from a cryptographically secure random source. Memos are not derived from your identifiers.
- **Never recycled.** A memo is minted once, for one instruction, and is never reissued — not after `DELETE`, not after `CANCELED`.


**Buy flow** — you declare the wire, the platform matches it:

1. You `PUT` a fiat instruction (client-generated UUID) with `currency` + destination `walletRipId`.
2. Ripple mints a memo, records the instruction as `UNUSED`, and returns it.
3. You wire USD to Ripple with the memo verbatim in the reference field.
4. Ripple scans the arriving wire for a memo (reference, description, and payment-memo free-text fields, in that order; first match wins). A `RIPxxxxxxx` wallet ripId, if present, takes precedence over a memo.
5. The matched instruction is claimed (`UNUSED` → `USED`).
6. RLUSD is minted to the wallet from your instruction. The resulting `ISSUANCE` transaction carries `fiatInstructionId` + `memo` on its `FIAT` source leg.


**Redemption flow** — the platform generates the instruction for you:

1. You burn RLUSD on-chain (the existing redemption flow — nothing changes for you).
2. Ripple registers the outbound fiat payment for that burn and mints a memo for it, creating a `FIAT`-destination instruction bound to the payment.
3. The memo is then visible to you in three places you already consume: `GET /v1/stablecoin/transactions/{id}` (`destination.memo` + `destination.fiatInstructionId`), the next `STABLECOIN_TRANSACTION` webhook, and `GET /v1/stablecoin/fiat-instructions/{id}`.
4. Ripple sends the outbound USD wire with the same memo verbatim in its reference field.


## Reconciliation guidance

Run one extractor — `RL[0-9A-HJKMNP-TV-Z]{14}` — over the reference/description/memo fields of every wire in your bank feed, in and out. Any hit is an RLUSD memo:

- A memo on an **outbound** wire you sent → your buy instruction's memo. Look for an `ISSUANCE` transaction whose `source.memo` equals it.
- A memo on an **inbound** wire from Ripple → a redemption memo. Look for a `REDEMPTION` transaction whose `destination.memo` equals it.


Because memos are globally unique, a memo maps to at most one transaction and you never need the direction to disambiguate. Match on the full 16-character token by exact string equality after extraction; do not substring-match a partial memo.