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. For every operation and field, see the Ripple Custody (Wallet) API reference. Before you go live, read Behaviors to know.
| Environment | Base URL |
|---|---|
| Sandbox | https://api.sandbox.palisade.co |
| Production | https://api.palisade.co |
- Every path is versioned with a
/v2prefix, for examplehttps://api.sandbox.palisade.co/v2/vaults. - Request and response bodies are JSON. Send
Content-Type: application/jsonon requests that have a body. - Field names in request and response bodies use camelCase, for example
externalIdandpageSize.
The API authenticates requests with short-lived access tokens that you get by exchanging an API credential.
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, Manage API credentials, and API credentials best practices.
Send the credential's
clientIdandclientSecretin aPOSTrequest to/v2/credentials/oauth/token. This endpoint doesn't take anAuthorizationheader.The response returns
accessToken,tokenType(Bearer),expiresIn(the number of seconds until the token expires), andscope. Every credential type currently returns the samescopevalue,credential:read:credential::*: it doesn't list the credential's permissions, which Ripple Custody (Wallet) checks on the server for each request. Don't usescopeto work out what a credential can do.If the
clientIdorclientSecretis wrong, the exchange returns400with reasonPAL000.020, not401.Send the token on every other request in an
Authorizationheader:Authorization: Bearer <accessToken>
For a complete curl example, see Step 2: Authenticate.
| 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.
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.
List operations return results one page at a time.
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/webhooksandPOST /v2/webhooks:listGET /v2/webhooks/{webhookId}/subscriptionsandPOST /v2/webhooks/{webhookId}/subscriptions:listGET /v2/counterparties/{counterpartyId}/addressesandPOST /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.
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.
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.
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.
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, andendsWithare case-insensitive.between.startandbetween.endare inclusive.relativeDayscounts days from now: negative values are in the past. For example,filter.createdAt.relativeDays=-7covers the last seven days.- For
inandnotIn, repeat the parameter once for each value, for examplefilter.blockchain.in=ETHEREUM&filter.blockchain.in=XRP_LEDGER. - Quantity filters take the quantity as a decimal string.
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.
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.
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.
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:
{
"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.
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.
externalIdis optional. An empty string ornullcounts as no value, and a request without anexternalIdalways creates a new object.- It's 1 to 64 characters long. A longer value returns
400with reasonPAL000.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, Create a wallet, or Create a transfer:
{
"name": "Treasury",
"externalId": "95a770cd-8766-4dd5-938c-3c10ead1c4b1"
}{
"name": "USD Issuance",
"blockchain": "AVALANCHE",
"keystore": "HSM",
"externalId": "95a770cd-8766-4dd5-938c-3c10ead1c4b1"
}{
"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.
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.
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.
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.
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.
To follow an object to its final status, either poll it with its GET operation or subscribe to 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.
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. For an example error body, the full gRPC-to-HTTP status mapping, and the meaning of each PAL reason code, see 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.
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.
- 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
networkFeefield, are also in whole asset units, such as ETH on Ethereum. - EVM gas fields are the exception:
gasPrice,baseFee, andpriorityFeein fee estimates are in wei, andgasLimitis 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 examplefilter.createdAt.after=2026-07-23T10:20:00Z. - Durations, such as the
durationof a rolling wallet limit, are strings with anssuffix that give the number of seconds, for example"86400s"for 24 hours.
Plan for these behaviors before you go live. For the fix for a specific error, see Troubleshoot common errors.
PUTrequests replace the whole object. A field you omit isn't left unchanged: updating a vault withoutcorrelationIdclears it, and updating wallet settings without a setting sets that setting tofalse. Always send the complete object, with the current values of the fields you aren't changing. See Update a vault and Update wallet settings.- A reused
externalIdreturns the original object. A create request with anexternalIdthat's already in use returns the object created the first time, with200, so retries are safe. Reusing a transfer'sexternalIdon a different wallet returns404with reasonPAL010.004instead. See External IDs. - A transfer that your policies reject still returns
200. Ripple Custody (Wallet) creates the transaction, which moves toREJECTEDwithin moments. Read the cause from itsproblemsarray. See Track the transaction lifecycle.