# 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](/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](/products/stablecoin/api/transactions#get-transaction) shape.
- **Same webhooks.** A `STABLECOIN_TRANSACTION` webhook is delivered for every state change, with the same [Webhook Payload](/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`.
- **A UAT-only `POST` operation.** Because no real RLUSD flows through UAT, you create your own sample transactions with `POST /v1/stablecoin/transactions` (see [Generate sample transactions](#generate-sample-transactions)). This API operation does **not** exist in Production.
- **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 are scoped to your tenant and are independent of production data.


## 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](/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 |


## 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` |
| `walletId` | 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](/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",
    "walletId": "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",
    "walletId": "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",
    "walletId": "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](/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",
  "walletId": "RIP2330137"
}
```

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

## 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., `walletId` provided when `type` is `FIAT`, or missing `walletId` when `type` is `CRYPTO`) |
| `UNKNOWN_EXCEPTION` | 500 | Unexpected server error |