# UAT (sandbox) environment

The UAT (user acceptance testing) environment provides a sandbox that lets you build and validate your RLUSD integration end-to-end **without moving real funds or touching mainnet**. It is backed by a mock service that mimics the production transaction lifecycle and fires the same webhooks, so the code you write against UAT works unchanged in the Production environment.

Get UAT API credentials
To generate UAT API credentials, follow the same steps as in [Getting Started](/pt-br/products/stablecoin/api/get-started#generate-api-credentials), and select **UAT** in the environment dropdown instead of **Production**.

## What's the same as Production

- **Same paths and contract.** The read and approve endpoints live at the same paths as Production (`/v1/stablecoin/transactions`, `/v1/stablecoin/transactions/{id}`, `/v1/stablecoin/transactions/{id}/destination`) and return the same [Transaction Object](/pt-br/products/stablecoin/api/transactions#get-transaction) shape.
- **Same webhooks.** A `STABLECOIN_TRANSACTION` webhook is delivered for every state change, with the same [Webhook Payload](/pt-br/products/stablecoin/api/webhooks#webhook-payload) structure and the same at-least-once delivery and retry semantics.
- **Same OAuth model.** Client-credentials grant with `rlusd_customers:read` and `rlusd_customers:write` scopes.


## What's different in UAT

- **Different host.** All requests go to `https://api.test.ripple.com` instead of `https://api.ripple.com`.
- **UAT-only write operations.** Because no real RLUSD flows through UAT and no onboarding data is recorded there, you supply your own:
  - `POST /v1/stablecoin/transactions` creates sample transactions (see [Generate sample transactions](#generate-sample-transactions)).
  - `PUT /v1/stablecoin/wallets` and `PUT /v1/stablecoin/bank-accounts` supply the wallets and bank accounts the read operations then serve (see [Seed wallets and bank accounts](#seed-wallets-and-bank-accounts)).
Neither of these API operations exist in Prod.
- **Automatic completion.** A sample transaction created in `PROCESSING` auto-advances to `COMPLETED` after a short delay (≈ **2 seconds**), emitting the corresponding webhooks. There is no real settlement wait.
- **Testnet network names.** Crypto endpoints use testnet network identifiers — e.g. `XRPL_TESTNET`, `ETH_SEPOLIA`, `BASE_SEPOLIA` (and other `*_SEPOLIA` / `*_TESTNET` networks) — rather than the Production `XRPL` / `ETH` values. Generated `transactionHash`, `address`, and `counterpartyAddress` values are mock artifacts, not real on-chain data.
- **No `CANCELED` simulation.** The mock lifecycle only drives `PROCESSING → COMPLETED` (and `PENDING → PROCESSING → COMPLETED` for flexible redemption). `CANCELED` and `FAILED` are not produced by UAT.
- **Tenant-scoped, isolated data.** Sample transactions, wallets and bank accounts are scoped to your tenant and are independent of production data.
- **Wallets and bank accounts are seeded, not onboarded.** In Production these come from your Ripple onboarding records. In UAT `GET /v1/stablecoin/wallets` and `GET /v1/stablecoin/bank-accounts` return exactly what you supplied via their `PUT` counterparts, and an empty list until you do.


## Authentication (UAT)

Obtain a UAT access token from the UAT token endpoint with a UAT audience:

**`POST https://api.test.ripple.com/v2/oauth/token`**

```http
POST /v2/oauth/token HTTP/1.1
Host: api.test.ripple.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=<your_uat_client_id>
&client_secret=<your_uat_client_secret>
&audience=urn:ripplexcurrent-uat:<your_tenant_id>
```

The environment component of the `audience` URN must match the environment you're authenticating against: use the `-uat` suffix (`urn:ripplexcurrent-uat:<your_tenant_id>`) for UAT, in contrast to the `-prod` value used in [Production](/pt-br/products/stablecoin/api/authentication). A token requested with the wrong audience is rejected.

Use the returned `access_token` as a `Bearer` token on every UAT request, exactly as in Production.

## API operations

**Base URL:** `https://api.test.ripple.com`

| Method | Path | Scope Required | Description |
|  --- | --- | --- | --- |
| `POST` | `/v1/stablecoin/transactions` | `rlusd_customers:write` | **UAT only.** Create a sample transaction (see scenarios below) |
| `GET` | `/v1/stablecoin/transactions` | `rlusd_customers:read` | List transactions (paginated) |
| `GET` | `/v1/stablecoin/transactions/{id}` | `rlusd_customers:read` | Get a specific transaction |
| `PUT` | `/v1/stablecoin/transactions/{id}/destination` | `rlusd_customers:write` | Approve a pending (flexible) redemption |
| `PUT` | `/v1/stablecoin/fiat-instructions/{id}` | `rlusd_customers:write` | Create a buy instruction and get a memo |
| `GET` | `/v1/stablecoin/fiat-instructions/{id}` | `rlusd_customers:read` | Get one instruction |
| `GET` | `/v1/stablecoin/fiat-instructions` | `rlusd_customers:read` | List instructions (paginated) |
| `DELETE` | `/v1/stablecoin/fiat-instructions/{id}` | `rlusd_customers:write` | Delete an unused buy instruction |
| `GET` | `/v1/stablecoin/wallets` | `rlusd_customers:read` | List your wallets (returns testnet chains) |
| `PUT` | `/v1/stablecoin/wallets` | `rlusd_customers:write` | **UAT only.** Replace your entire wallet list (see [Seed wallets and bank accounts](#seed-wallets-and-bank-accounts)) |
| `GET` | `/v1/stablecoin/bank-accounts` | `rlusd_customers:read` | List your bank accounts |
| `PUT` | `/v1/stablecoin/bank-accounts` | `rlusd_customers:write` | **UAT only.** Replace your entire bank-account list (see [Seed wallets and bank accounts](#seed-wallets-and-bank-accounts)) |
| `GET` | `/v1/stablecoin/crypto-instructions` | `rlusd_customers:read` | List Ripple's redemption/bridge destinations |
| `GET` | `/v1/stablecoin/fiat-bank-details` | `rlusd_customers:read` | Get Ripple's bank details (returns a fixed fixture bank in UAT) |


## Generate sample transactions

**`POST /v1/stablecoin/transactions`** *(UAT only)*

**Required Scope:** `rlusd_customers:write`

**Request Body Fields:**

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| `type` | string | No | `ISSUANCE`, `REDEMPTION`, or `BRIDGE`. When **present**, the transaction starts in `PROCESSING` and auto-completes. When **omitted**, the transaction lands in `PENDING` to simulate a flexible redemption awaiting approval |
| `currencyCode` | string | Yes | The token. Use `RLUSD` |
| `amount` | number | Yes | Transaction amount |
| `source` | object | Yes* | Source [endpoint](#uat-endpoint-fields). Required for `REDEMPTION`, `BRIDGE`, and `PENDING` scenarios |
| `destination` | object | Yes* | Destination [endpoint](#uat-endpoint-fields). Required for `ISSUANCE`, `REDEMPTION`, and `BRIDGE` |


**Endpoint object fields (`source` / `destination`):**

| Field | Type | Description |
|  --- | --- | --- |
| `type` | string | `FIAT` or `CRYPTO` |
| `walletRipId` | string | Wallet identifier (`RIPxxxxxxx`). Use on a `CRYPTO` endpoint that references one of your wallets |
| `address` | string | A raw on-chain address. Use on a `CRYPTO` endpoint to simulate an external/customer address |
| `chain` | string | Testnet network name (e.g. `XRPL_TESTNET`, `ETH_SEPOLIA`, `BASE_SEPOLIA`). Defaults to `XRPL_TESTNET` for `CRYPTO` endpoints when omitted |


**Response:** A [Transaction Object](/pt-br/products/stablecoin/api/transactions#get-transaction) reflecting the newly created sample transaction. For typed transactions it is returned in `PROCESSING` and will transition to `COMPLETED` (with mock `transactionHash` / `counterpartyAddress` populated) after ≈ 2 seconds, driving the usual webhooks.

### Scenario 1 — Issuance (USD → RLUSD)

Simulates fiat received and RLUSD minted to one of your wallets.

```http
POST /v1/stablecoin/transactions HTTP/1.1
Host: api.test.ripple.com
Authorization: Bearer <uat_access_token>
Content-Type: application/json
```

```json
{
  "type": "ISSUANCE",
  "currencyCode": "RLUSD",
  "amount": 500,
  "source": {
    "type": "FIAT"
  },
  "destination": {
    "type": "CRYPTO",
    "walletRipId": "RIP1136301"
  }
}
```

**Webhooks fired:** `PROCESSING` immediately, then `COMPLETED` (≈ 2s later) with `eventData.destination.hash` populated.

### Scenario 2 — Redemption (RLUSD → USD)

Simulates inbound RLUSD from one of your wallets being redeemed to fiat.

```json
{
  "type": "REDEMPTION",
  "currencyCode": "RLUSD",
  "amount": 250.75,
  "source": {
    "type": "CRYPTO",
    "walletRipId": "RIP1136301",
    "chain": "XRPL_TESTNET"
  },
  "destination": {
    "type": "FIAT"
  }
}
```

**Webhooks fired:** `PROCESSING` (with `eventData.source.hash` populated), then `COMPLETED`.

### Scenario 3 — Bridge (cross-chain RLUSD)

Simulates a cross-chain transfer from an external address into one of your wallets on another chain.

```json
{
  "type": "BRIDGE",
  "currencyCode": "RLUSD",
  "amount": 1000,
  "source": {
    "type": "CRYPTO",
    "address": "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH",
    "chain": "XRPL_TESTNET"
  },
  "destination": {
    "type": "CRYPTO",
    "walletRipId": "RIP1136301",
    "chain": "BASE_SEPOLIA"
  }
}
```

**Webhooks fired:** `PROCESSING`, then `COMPLETED` with `eventData.destination.hash` (the destination-leg hash) populated.

### Scenario 4 — Pending (Flexible Redemption)

Omit `type` to simulate inbound RLUSD held in `PENDING`, awaiting your approval. This mirrors the [Flexible Redemption](/pt-br/products/stablecoin/api/transactions#flexible-redemption) flow.

**Step 1 — Create the pending transaction:**

```json
{
  "currencyCode": "RLUSD",
  "amount": 1000,
  "source": {
    "type": "CRYPTO",
    "address": "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH",
    "chain": "XRPL_TESTNET"
  }
}
```

The response is returned with `status: "PENDING"` and a null `type`/`destination`. A `PENDING` webhook is fired. Capture the returned `id`.

**Step 2 — Approve it** via the standard approve endpoint:

**`PUT /v1/stablecoin/transactions/{id}/destination`**

*Approve as a fiat redemption:*

```json
{
  "type": "FIAT"
}
```

*…or approve as a crypto bridge:*

```json
{
  "type": "CRYPTO",
  "walletRipId": "RIP2330137"
}
```

**Webhooks fired:** `PENDING` on creation, then `PROCESSING` once approved (with `eventData.type` now set to `REDEMPTION` or `BRIDGE`), then `COMPLETED`.

## Seed wallets and bank accounts

In production your wallets and bank accounts come from your Ripple onboarding records. UAT has no onboarding, so you supply them yourself and the read endpoints then serve them back unchanged.

These operations replace the whole list
Anything you leave out of the request body is **deleted** — a partial list is not a partial update. Always send your complete desired list. Sending `[]` clears everything, which is also how you reset between test runs; seeded data is otherwise never removed automatically.

Both operations require the `rlusd_customers:write` scope, are **UAT only**, accept at most **50 records** per request, and return the resulting list.

### Replace wallets

**`PUT /v1/stablecoin/wallets`** *(UAT only)*

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| `ripId` | string | No | Omit to create a wallet and have Ripple assign the id. Supply an existing `ripId` to update that wallet in place, keeping its original `createdAt`. Supply an unseen one to create a wallet under it |
| `address` | string | Yes | On-chain address. Validated against `chain`; stored normalized, so an EVM address comes back `0x`-prefixed |
| `chain` | string | Yes | Testnet network name, e.g. `XRPL_TESTNET`, `ETH_SEPOLIA`, `BASE_SEPOLIA` |
| `nickname` | string | Yes | Your own name for the wallet |
| `destinationTag` | number | No | XRPL destination tag |


There is no `status` field: a seeded wallet is always returned `APPROVED`, since only an approved wallet can be used for RLUSD movements.

```json
[
  {
    "nickname": "Treasury XRPL",
    "address": "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH",
    "destinationTag": 1234567,
    "chain": "XRPL_TESTNET"
  },
  {
    "nickname": "Treasury Ethereum",
    "address": "0x1111111111111111111111111111111111111111",
    "chain": "ETH_SEPOLIA"
  }
]
```

Both wallets above omit `ripId`, so Ripple assigns one to each — an unpredictable `RIP` + 7-digit id such as `RIP0568238`. Read them off the response rather than assuming a pattern, then use them in later requests: as the `walletRipId` on a transaction endpoint, as the `?ripId=` filter on [`GET /v1/stablecoin/wallets`](/pt-br/products/stablecoin/api/wallets), or in a follow-up `PUT` to edit the wallet rather than replace it.

**Response:** an array of [Wallet objects](/pt-br/products/stablecoin/api/wallets#wallet-object).

### Replace bank accounts

**`PUT /v1/stablecoin/bank-accounts`** *(UAT only)*

| Field | Type | Required | Description |
|  --- | --- | --- | --- |
| `name` | string | Yes | Your name for the account |
| `bankName` | string | Yes | Bank the account is held at |
| `accountNumber` | string | No | The **whole** account number. Returned as `accountNumberLastFour` |
| `iban` | string | No | The **whole** IBAN. Returned as `ibanLastFour` |
| `swiftBicCode` | string | No |  |
| `routingNumber` | string | No |  |
| `currency` | string | Yes | ISO-4217 code |
| `reference` | string | No |  |
| `isRedemption` | boolean | No | Defaults to `false`. **At most one account per request may set it** |


Account numbers and IBANs are supplied **whole** and only their last four characters are ever returned, so a seeded account is redacted exactly as a real one is.

There is no per-account update: nothing in the bank-account contract identifies an account, so the list you send becomes the list of your accounts outright. Moving `isRedemption` to a different account is just another replace.

```json
[
  {
    "name": "Treasury Operating",
    "bankName": "First National Bank",
    "accountNumber": "1234567890",
    "swiftBicCode": "FNBKUS33",
    "routingNumber": "021000021",
    "currency": "USD",
    "reference": "f00840c9-2897-422e-9ca7-2727e7912557",
    "isRedemption": true
  },
  {
    "name": "USD Settlement",
    "bankName": "Second National Bank",
    "iban": "GB33BUKB20201555554321",
    "swiftBicCode": "SNBKGB2L",
    "currency": "USD"
  }
]
```

The first account above comes back with `"accountNumberLastFour": "7890"` and the second with `"ibanLastFour": "4321"`.

**Response:** an array of [Bank account objects](/pt-br/products/stablecoin/api/bank-accounts#bank-account-object).

### Errors

| Status | Code | When |
|  --- | --- | --- |
| `400` | `MOCK_RECORD_LIMIT_EXCEEDED` | More than 50 records in one request |
| `400` | `INVALID_REQUEST_BODY` | Malformed body, an `address` that is not valid for its `chain`, an unrecognised `chain`, the same `ripId` twice in one list, a missing `name`/`bankName`/`currency`, a missing wallet `nickname`, or more than one account setting `isRedemption` |
| `403` | — | Token lacks `rlusd_customers:write` |


## Error handling

Errors follow [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457.html) (the successor to RFC 7807) and include a `code` field for programmatic error identification.

**Example error response:**

```json
{
  "type": "https://docs.ripple.com/products/stablecoin",
  "title": "Not Found",
  "status": 404,
  "code": "TRANSACTION_NOT_FOUND"
}
```

### HTTP status codes

| HTTP Status | Description |
|  --- | --- |
| `400 Bad Request` | Invalid request parameters or body |
| `401 Unauthorized` | Missing or expired access token |
| `403 Forbidden` | Insufficient scope for the requested operation |
| `404 Not Found` | Resource does not exist |
| `409 Conflict` | Invalid state transition |
| `500 Internal Server Error` | Server error; contact support if persistent |


### Error codes

| Code | HTTP Status | Description |
|  --- | --- | --- |
| `TRANSACTION_NOT_FOUND` | 404 | Transaction ID not found |
| `TRANSACTION_INVALID_STATUS_TRANSITION` | 409 | Cannot update a transaction that is not in `PENDING` status (e.g., approving an already `PROCESSING` or `COMPLETED` transaction) |
| `PENDING_REDEMPTION_TRANSACTION_DESTINATION_TYPE_IMMUTABLE` | 409 | A destination has already been chosen for this pending redemption and cannot be changed. Submitting the *same* destination again is idempotent and returns `200 OK`; only a *different* destination produces this error |
| `INVALID_REQUEST_BODY` | 400 | Malformed or invalid request body (e.g., `walletRipId` provided when `type` is `FIAT`, or missing `walletRipId` when `type` is `CRYPTO`) |
| `MOCK_RECORD_LIMIT_EXCEEDED` | 400 | **UAT only.** A `PUT /v1/stablecoin/wallets` or `PUT /v1/stablecoin/bank-accounts` body carrying more than 50 records. See [Seed wallets and bank accounts](#seed-wallets-and-bank-accounts) |
| `UNKNOWN_EXCEPTION` | 500 | Unexpected server error |