# API conventions

These conventions apply across the Ripple Custody (Wallet) API. Each API guide links here instead of repeating them. For a step-by-step first integration, see [Getting started with the API](/products/wallet/get-started/use-the-api). For every operation and field, see the [Ripple Custody (Wallet) API reference](/products/wallet/api-docs/palisade-api/palisade-api). Before you go live, read [Behaviors to know](#behaviors-to-know).

## Base URLs and request format

| Environment | Base URL |
|  --- | --- |
| Sandbox | `https://api.sandbox.palisade.co` |
| Production | `https://api.palisade.co` |


- Every path is versioned with a `/v2` prefix, for example `https://api.sandbox.palisade.co/v2/vaults`.
- Request and response bodies are JSON. Send `Content-Type: application/json` on requests that have a body.
- Field names in request and response bodies use camelCase, for example `externalId` and `pageSize`.


## Authentication

The API authenticates requests with short-lived access tokens that you get by exchanging an API credential.

### Credential types

You create API credentials in the console. Each credential has exactly one type, and the type decides which endpoints its permissions can cover:

| Credential type | Covers |
|  --- | --- |
| `WALLETS` | Vaults, wallets, and balances |
| `TRANSACTIONS` | Transactions, including freezes and sweep triggers |
| `CONTROLS` | Counterparties, address book entries, and policy rules |
| `MONITORING` | Webhooks and webhook subscriptions |


The console labels the fourth type **Monitoring**, but API responses return its category as `AUDIT`. They're the same type. An integration that touches several areas needs one credential, and one token, for each type.

For how to create, scope, and rotate credentials, see [API credentials overview](/products/wallet/organization-and-access/api-credentials), [Manage API credentials](/products/wallet/organization-and-access/api-credentials/manage-api-credentials), and [API credentials best practices](/products/wallet/organization-and-access/api-credentials/best-practices).

### Get and use an access token

1. Send the credential's `clientId` and `clientSecret` in a `POST` request to `/v2/credentials/oauth/token`. This endpoint doesn't take an `Authorization` header.
2. The response returns `accessToken`, `tokenType` (`Bearer`), `expiresIn` (the number of seconds until the token expires), and `scope`. Every credential type currently returns the same `scope` value, `credential:read:credential::*`: it doesn't list the credential's permissions, which Ripple Custody (Wallet) checks on the server for each request. Don't use `scope` to work out what a credential can do.
If the `clientId` or `clientSecret` is wrong, the exchange returns `400` with reason `PAL000.020`, not `401`.
3. Send the token on every other request in an `Authorization` header:

```
Authorization: Bearer <accessToken>
```


For a complete `curl` example, see [Step 2: Authenticate](/products/wallet/get-started/use-the-api#step-2-authenticate).

### Token lifetime and renewal

| Environment | Token lifetime |
|  --- | --- |
| Production | 60 seconds |
| Sandbox | 3,600 seconds (1 hour) |


Read the lifetime from `expiresIn` rather than hard-coding it. Until a token expires, the token endpoint returns the same token again, so `expiresIn` is the time remaining and is often less than the full lifetime. The token exchange doesn't return a refresh token. Reuse a token until it expires, and then call `/v2/credentials/oauth/token` again for a new one.

Request tokens only from the token endpoint
The token endpoint caches tokens on the server, so you don't need to build your own token cache. Requesting tokens any other way uses a limited issuance quota and can cause authentication failures for your whole organization. See [Use API credentials](/products/wallet/organization-and-access/api-credentials/manage-api-credentials#use-api-credentials-via-the-api).

## Pagination

List operations return results one page at a time.

### Request parameters

Most list operations accept two forms of paging parameters:

| Parameter | Description |
|  --- | --- |
| `pagination.pageSize` | The number of results for each page. |
| `pagination.pageToken` | The token for the page you want, taken from an earlier response. |
| `pageSize`, `pageToken` | Older top-level forms of the same parameters. Not every list operation accepts them. |


The default page size is 50. The maximum is 1,000 for most list operations. The reference documents a lower maximum of 100 for the top-level `pageSize` parameter on these operations:

- `GET /v2/webhooks` and `POST /v2/webhooks:list`
- `GET /v2/webhooks/{webhookId}/subscriptions` and `POST /v2/webhooks/{webhookId}/subscriptions:list`
- `GET /v2/counterparties/{counterpartyId}/addresses` and `POST /v2/counterparties/{counterpartyId}/addresses:list`


On those same operations, the reference lists `pagination.pageSize` with a maximum of 1,000. On `GET /v2/webhooks`, a top-level `pageSize` above 100 returns `400`, and `pagination.pageSize` accepts up to 1,000. To request more than 100 results, use the `pagination.*` form. A page size above the maximum returns `400` with reason `PAL000.020`.

Send one form of paging parameters
The reference documents which form wins only for wallet transaction lists (`/v2/vaults/{vaultId}/wallets/{walletId}/transactions`) and sweep transaction lists (`/v2/transactions/sweep/{sweepId}`): there, `pagination` overrides the top-level `pageSize` and `pageToken`, which are deprecated on the wallet transaction list. For other operations, the precedence isn't documented. Send either the `pagination.*` form or the top-level form in a request, never both. Some operations, such as `GET /v2/transactions` and wallet balances, accept only the `pagination.*` form, so use `pagination.pageSize` and `pagination.pageToken` everywhere.

### Response fields

List responses return the page of results in a named array, such as `vaults`, `wallets`, or `transactions`, and paging metadata in a `filter` object:

| Field | Description |
|  --- | --- |
| `filter.total` | The total number of results that match the request. Always present. |
| `filter.nextPageToken` | The token for the next page. Present when there's another page. |
| `filter.previousPageToken` | The token for the previous page. Present when there's a previous page. |


To get the next page, send the same request again with `filter.nextPageToken` as `pagination.pageToken` (or as `pageToken`, if you use the top-level form). When the response has no `nextPageToken`, you have the last page.

The wallet balances operation (`GET /v2/vaults/{vaultId}/wallets/{walletId}/balances`) is the exception: it returns the same metadata in a `pagination` object instead of `filter`, and only for paged requests. See [Filter, search, and page balances](/products/wallet/vaults-and-wallets/wallets/understanding-balances#filter-search-and-page-balances).

## Filtering, search, and sorting

### Field filters

Filter a `GET` list with query parameters in the form `filter.<field>.<operator>=<value>`, for example `filter.name.contains=treasury`. All the filters in a request are combined with AND. Each operation's filterable fields are listed in the [API reference](/products/wallet/api-docs/palisade-api/palisade-api).

The operators a field accepts depend on its type:

| Field type | Operators | Example fields |
|  --- | --- | --- |
| Text | `eq`, `notEq`, `contains`, `startsWith`, `endsWith`, `in`, `notIn`, `isNull` | `id`, `name`, `externalId`, `correlationId`, `address` |
| Timestamp | `eq`, `notEq`, `before`, `after`, `gte`, `lte`, `between.start`, `between.end`, `isNull`, `relativeDays` | `createdAt`, `updatedAt` |
| Enumerated value | `eq`, `notEq`, `in`, `notIn` | `blockchain`; transaction `status` and `type` |
| Quantity | `eq`, `notEq`, `gt`, `gte`, `lt`, `lte`, `in`, `notIn`, `isNull` | Transaction `qty` |
| Boolean | `eq`, `isNull` | `active` |


- `contains`, `startsWith`, and `endsWith` are case-insensitive.
- `between.start` and `between.end` are inclusive.
- `relativeDays` counts days from now: negative values are in the past. For example, `filter.createdAt.relativeDays=-7` covers the last seven days.
- For `in` and `notIn`, repeat the parameter once for each value, for example `filter.blockchain.in=ETHEREUM&filter.blockchain.in=XRP_LEDGER`.
- Quantity filters take the quantity as a decimal string.


### Filters that take a bare value

Some filters don't take an operator. Set them directly, for example `filter.status=PROVISIONED`:

| Filter | Operations |
|  --- | --- |
| `filter.status` | Wallet lists, address lists, policy rule lists, and wallet limit lists |
| `filter.type` | Counterparty lists |
| `filter.limitType` | Wallet limit lists |
| `filter.kind`, `filter.trigger` | Policy rule lists |
| `filter.domain`, `filter.scope` | Webhook subscription lists |


Transaction lists are different: their `status` filter takes operators, such as `filter.status.eq=CONFIRMED`.

A few list operations also accept older top-level query parameters, such as `blockchain`, `address`, and `correlationId`, beside the `filter.*` equivalents. On the wallet transaction list, the top-level `correlationId` is deprecated in favor of `filter.correlationId`. Prefer the `filter.*` form, and don't combine the two forms for the same field in one request.

### Search

Most list operations accept a `search` parameter: a case-insensitive match across several fields at once. The fields differ by operation. For example, the vault list searches vault ID, name, description, external ID, and correlation ID. The API reference lists the fields for each operation.

### Sorting

Set `pagination.orderBy` to a sortable field and `pagination.order` to `SORT_ORDER_ASC` or `SORT_ORDER_DESC`. Sortable field names are written in snake_case, such as `created_at`, and differ by operation. For example, the vault list sorts by `name`, `created_at`, or `updated_at`, and transaction lists sort by `created_at`, `updated_at`, `amount`, or `status`. The API reference lists each operation's sortable fields in the description of its `pagination` request body field.

### List with a request body

Most list resources also have a `POST` variant whose path ends in `:list`, such as `POST /v2/vaults:list` or `POST /v2/transactions:list`. It takes the same `search`, `filter`, and `pagination` options as a JSON body instead of query parameters, which suits long or complex filters:

```json
{
  "search": "treasury",
  "filter": {
    "createdAt": { "relativeDays": -7 }
  },
  "pagination": { "pageSize": 50, "orderBy": "created_at", "order": "SORT_ORDER_DESC" }
}
```

It returns the same response as the matching `GET` list.

## Identifiers and retries

### External IDs

`externalId` is an identifier that you assign when you create a vault, a wallet, or a transfer. It makes the create request idempotent: if you send the same create request again with the same `externalId`, Ripple Custody (Wallet) returns the object it created the first time instead of creating a duplicate. This protects against accidental duplicate requests, for example a retry after a timeout. Without an `externalId`, Ripple Custody (Wallet) can't tell a deliberate repeat from an accidental one, and it runs every request.

- `externalId` is optional. An empty string or `null` counts as no value, and a request without an `externalId` always creates a new object.
- It's 1 to 64 characters long. A longer value returns `400` with reason `PAL000.020`.
- It must be unique across your organization. For example, two wallets can't have transactions with the same `externalId`.


Assign a new `externalId` to each object you intend to create, and reuse it only when you retry the same request. The response includes the `externalId` you set.

Set `externalId` in the request body of [Create a vault](/products/wallet/vaults-and-wallets/vaults/manage-vaults#create-a-vault-via-the-api), [Create a wallet](/products/wallet/vaults-and-wallets/wallets/manage-wallets-api#create-a-wallet-via-the-api), or [Create a transfer](/products/wallet/transactions/send-assets#create-a-transfer-via-the-api):

```json
{
  "name": "Treasury",
  "externalId": "95a770cd-8766-4dd5-938c-3c10ead1c4b1"
}
```

```json
{
  "name": "USD Issuance",
  "blockchain": "AVALANCHE",
  "keystore": "HSM",
  "externalId": "95a770cd-8766-4dd5-938c-3c10ead1c4b1"
}
```

```json
{
  "destinationAddress": "rMhieePimPBNQZByxQxX1HXY6VyxEep2JE",
  "qty": "0.000003",
  "symbol": "XRP",
  "externalId": "95a770cd-8766-4dd5-938c-3c10ead1c4b1"
}
```

A repeated create request with the same `externalId` returns the same vault or wallet. For a transfer on the same wallet, it returns `200` and the original transaction in its current state. The API doesn't compare the rest of the request body: a retry with a different `qty` or destination still returns the original transaction and creates nothing new. A Create Transfer request on a different wallet that reuses the `externalId` doesn't create a transaction: it returns `404` with the message `transaction not found` and reason `PAL010.004`.

### Correlation IDs

`correlationId` is an identifier that links related actions, for example the vault, wallets, and transfers that belong to one of your workflows. You can set it when you create or update a vault or wallet and when you create a transfer, and then find the related objects with `filter.correlationId` or `search`. The API reference doesn't document `correlationId` as unique or as making a request idempotent, so don't rely on it to prevent duplicates: use `externalId` instead.

### Freeze and unfreeze retries

The freeze and unfreeze operations accept an optional `mutationId` query parameter: a stable identifier to reuse when you retry the same freeze or unfreeze request. See [Freeze and unfreeze transactions via the API](/products/wallet/compliance/transaction-freeze-api).

For other write operations, the API reference doesn't define retry or idempotency behavior. Before you retry a create request that didn't return a response, list or search for the object to check whether the first request succeeded.

## Asynchronous operations and approvals

Many write operations return as soon as Ripple Custody (Wallet) accepts the request, before the change is complete. The returned object carries a status that moves on later:

| Operation | Initial status | Next steps |
|  --- | --- | --- |
| Create a wallet | `CREATED` | Provisioning runs until `PROVISIONED` or `PROVISIONING_FAILED`. MPC wallets also need quorum devices to approve key generation. |
| Add an address book address | `CREATION_APPROVAL_PENDING` | Waits for an **Addresses** approval group, if you have one, and then a compliance check. |
| Create a policy rule | `RULE_CREATION_APPROVAL_PENDING` | Waits for a **Policy rules** approval group, if you have one. |
| Create a transaction | `REQUESTED` | Moves through policy checks, approvals, signing, and publishing until it reaches `CONFIRMED`, `REJECTED`, or `FAILED`. |


Approvers act on pending requests in the console. If an approval group's timeout passes before it collects enough approvals, Ripple Custody (Wallet) rejects the action. See [Approvals](/products/wallet/governance-and-controls/approvals).

To follow an object to its final status, either poll it with its `GET` operation or subscribe to [webhooks](/products/wallet/integrations-and-monitoring/webhooks), which push transaction, wallet, and approval events to your server. Webhooks are the better fit for production integrations. When you poll, remember that production tokens last 60 seconds, so request a new token as each one expires.

## Errors

When a request fails, the API returns an HTTP error status and a JSON body with these fields:

| Field | Description |
|  --- | --- |
| `code` | A numeric error code. |
| `message` | A human-readable description of the error. |
| `details` | An array of objects, each identified by `@type`. An `ErrorInfo` entry carries a `reason` code (such as `PAL000.002`), a `domain`, and `metadata`. A `RequestInfo` entry carries the `requestId`. Not every error has both entries, and a `401` has no `details` at all. |


These are the error statuses you're most likely to receive:

| Status | Meaning |
|  --- | --- |
| `400` | The request is malformed or invalid, for example it fails field validation (reason `PAL000.020`), or the resource isn't in a state that allows the action. |
| `401` | The request is unauthenticated, for example because the access token is missing, malformed, or expired. Request a new token and try again. |
| `403` | The request is forbidden. Check that the credential's type and permissions cover the operation. |
| `404` | The resource doesn't exist. |
| `409` | An equivalent resource already exists, for example a webhook with the same name. |
| `501` | The path doesn't support the HTTP method, for example `PUT` on a webhook. |


Log the `requestId` from every error, and include it if you contact [Ripple Support](https://technicalsupport.ripple.com/). For an example error body, the full gRPC-to-HTTP status mapping, and the meaning of each `PAL` reason code, see [Error codes](/products/wallet/api-and-developer-experience/error-codes).

The reference doesn't document other statuses, such as `429` or `500`, or which errors are safe to retry. Treat any other status as an error. Before you retry a create request, see [Identifiers and retries](#identifiers-and-retries).

### Transaction problems

A transaction that fails carries a `problems` array that describes why. Each entry has `code`, `reason`, `domain`, `message`, `metadata`, and a `retryability` field set to `RETRYABLE` or `NOT_RETRYABLE`. In a problem, `domain` names the service that raised it, such as `policy-service`, rather than the API host.

## Numbers, units, and timestamps

- **Asset amounts** are decimal strings in whole asset units, for example `"qty": "0.01"` for 0.01 ETH. Send amounts as strings, not JSON numbers, so that no precision is lost. Wallet limits (`limitQty`) and balances use the same form.
- **Network fees**, in fee estimates and in the transfer `networkFee` field, are also in whole asset units, such as ETH on Ethereum.
- **EVM gas fields** are the exception: `gasPrice`, `baseFee`, and `priorityFee` in fee estimates are in wei, and `gasLimit` is in gas units. They're returned as strings.
- **64-bit integers**, such as `freezeRevision`, are returned as strings.
- **Timestamps** are RFC 3339 date-time strings, for example `2026-07-23T10:22:22.420Z`. Timestamp filters take the same format, for example `filter.createdAt.after=2026-07-23T10:20:00Z`.
- **Durations**, such as the `duration` of a rolling wallet limit, are strings with an `s` suffix that give the number of seconds, for example `"86400s"` for 24 hours.


## Behaviors to know

Plan for these behaviors before you go live. For the fix for a specific error, see [Troubleshoot common errors](/products/wallet/api-and-developer-experience/error-codes#troubleshoot-common-errors).

- **`PUT` requests replace the whole object.** A field you omit isn't left unchanged: updating a vault without `correlationId` clears it, and updating wallet settings without a setting sets that setting to `false`. Always send the complete object, with the current values of the fields you aren't changing. See [Update a vault](/products/wallet/vaults-and-wallets/vaults/manage-vaults#update-a-vault-via-the-api) and [Update wallet settings](/products/wallet/vaults-and-wallets/wallets/manage-wallets-api#update-wallet-settings-via-the-api).
- **A reused `externalId` returns the original object.** A create request with an `externalId` that's already in use returns the object created the first time, with `200`, so retries are safe. Reusing a transfer's `externalId` on a different wallet returns `404` with reason `PAL010.004` instead. See [External IDs](#external-ids).
- **A transfer that your policies reject still returns `200`.** Ripple Custody (Wallet) creates the transaction, which moves to `REJECTED` within moments. Read the cause from its `problems` array. See [Track the transaction lifecycle](/products/wallet/transactions/view-and-track-transactions#track-the-transaction-lifecycle-via-the-api).


## See also

- [Getting started with the API](/products/wallet/get-started/use-the-api)
- [Ripple Custody (Wallet) API reference](/products/wallet/api-docs/palisade-api/palisade-api)
- [Error codes](/products/wallet/api-and-developer-experience/error-codes)
- [Webhooks overview](/products/wallet/integrations-and-monitoring/webhooks)
- [Connect AI assistants](/products/wallet/api-and-developer-experience/connect-ai-assistants)