Skip to content

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.

Base URLs and request format

EnvironmentBase URL
Sandboxhttps://api.sandbox.palisade.co
Productionhttps://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 typeCovers
WALLETSVaults, wallets, and balances
TRANSACTIONSTransactions, including freezes and sweep triggers
CONTROLSCounterparties, address book entries, and policy rules
MONITORINGWebhooks 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.

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.

Token lifetime and renewal

EnvironmentToken lifetime
Production60 seconds
Sandbox3,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.

List operations return results one page at a time.

Request parameters

Most list operations accept two forms of paging parameters:

ParameterDescription
pagination.pageSizeThe number of results for each page.
pagination.pageTokenThe token for the page you want, taken from an earlier response.
pageSize, pageTokenOlder 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:

FieldDescription
filter.totalThe total number of results that match the request. Always present.
filter.nextPageTokenThe token for the next page. Present when there's another page.
filter.previousPageTokenThe 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.

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.

The operators a field accepts depend on its type:

Field typeOperatorsExample fields
Texteq, notEq, contains, startsWith, endsWith, in, notIn, isNullid, name, externalId, correlationId, address
Timestampeq, notEq, before, after, gte, lte, between.start, between.end, isNull, relativeDayscreatedAt, updatedAt
Enumerated valueeq, notEq, in, notInblockchain; transaction status and type
Quantityeq, notEq, gt, gte, lt, lte, in, notIn, isNullTransaction qty
Booleaneq, isNullactive
  • 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:

FilterOperations
filter.statusWallet lists, address lists, policy rule lists, and wallet limit lists
filter.typeCounterparty lists
filter.limitTypeWallet limit lists
filter.kind, filter.triggerPolicy rule lists
filter.domain, filter.scopeWebhook 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.

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:

{
  "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, 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.

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.

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:

OperationInitial statusNext steps
Create a walletCREATEDProvisioning runs until PROVISIONED or PROVISIONING_FAILED. MPC wallets also need quorum devices to approve key generation.
Add an address book addressCREATION_APPROVAL_PENDINGWaits for an Addresses approval group, if you have one, and then a compliance check.
Create a policy ruleRULE_CREATION_APPROVAL_PENDINGWaits for a Policy rules approval group, if you have one.
Create a transactionREQUESTEDMoves 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.

Errors

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

FieldDescription
codeA numeric error code.
messageA human-readable description of the error.
detailsAn 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:

StatusMeaning
400The 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.
401The request is unauthenticated, for example because the access token is missing, malformed, or expired. Request a new token and try again.
403The request is forbidden. Check that the credential's type and permissions cover the operation.
404The resource doesn't exist.
409An equivalent resource already exists, for example a webhook with the same name.
501The 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.

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.

  • 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 and Update wallet settings.
  • 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.
  • 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.

See also