Skip to content

This guide walks you through the Ripple Custody (Wallet) API from authentication to your first confirmed transaction. Every step includes a complete curl command you can run against the sandbox environment.

Console alternative

Prefer a visual interface? Follow the console getting-started guide instead.

What you can do with the API

The Ripple Custody (Wallet) API provides programmatic access to the platform, so you can integrate digital asset custody and management into your applications. Use it to:

  • Create and manage vaults and wallets
  • Initiate and monitor transactions
  • Configure transaction policies and address book entries
  • Set up webhooks for real-time notifications

Users, devices, MPC quorums, approval groups, and API credentials are managed in the console. For the full list of operations, see the Ripple Custody (Wallet) API reference.

To learn how API credentials work before you create one, see:

Prerequisites

Sign in to the Ripple Custody (Wallet) console at https://app.sandbox.palisade.co. See Set up your organization if this is your first login.

If you plan to use MPC wallets, set up at least one quorum first. See Configure devices and quorums. For sandbox testing, skip this and use HSM wallets instead.

Base URL

All examples in this guide use the sandbox environment:

https://api.sandbox.palisade.co

For production, replace with https://api.palisade.co.

For the conventions that every endpoint shares, such as token lifetime, pagination, filtering, and the error format, see API conventions.

Step 1: Create API credentials

Generate API credentials in the console to authenticate your API requests.

Owner or Administrator role required

Only users with the Owner or Administrator role can create API credentials. Other roles don't see Create credentials. If you don't see it, ask an owner or administrator in your organization.

  1. Navigate to Settings > API credentials.
  2. Click Create credentials. This opens the Permissions set page.
  3. Under Permission set type, select Wallets. Each type provides a different set of permissions:
Permission set typeWhat it grants
WalletsManage wallets and vaults
TransactionsManage transactions
ControlsManage policies and addresses
MonitoringWebhooks and auditing
  1. Under Name and description, enter a Name (for example, "Sandbox API - Wallets"). The Description is optional.

  2. Under IP addresses, select All IP addresses for sandbox testing. In production, use Limited IP addresses and enter a comma-separated list of IPs or CIDRs.

  3. Under Permission, build at least one permission. Each permission is built from these components:

    • Type — the resource category (balances, key, or vault)
    • Action — the operation (create, delete, read, or update)
    • Scope — how broadly the permission applies (org, vault, or key)
    • Resource — the specific vaults or wallets the permission applies to. Required when the scope is vault or key. An org scope applies to the whole organization and takes no resource.

    The options offered depend on the permission set type you selected, and the console doesn't add any permissions for you. Complete each part, then click Add another permission to add more, or Remove to delete one. Since this is your first credential and you haven't created vaults or wallets yet, set the scope to org so the credential works for all resources you create later.

  4. Click Generate credentials.

  5. Copy the clientId and clientSecret.

Store credentials securely

The clientSecret is shown only once. Store it in a secrets manager or environment variable — never commit it to source control.

Repeat steps 2–8 to create credentials for Transactions and Controls — you need all three permission set types to complete this walkthrough. Each credential has a single permission set type, so Add another permission only offers permissions from that type.

Scoping credentials in production

Once you have vaults and wallets, you can create narrower credentials scoped to specific resources. For example, a credential with vault scope and a specific vault selected under Resource only has access to that vault. See API credentials best practices.

Step 2: Authenticate

Exchange your API credentials for a Bearer token.

curl -X POST https://api.sandbox.palisade.co/v2/credentials/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "YOUR_CLIENT_ID",
    "clientSecret": "YOUR_CLIENT_SECRET"
  }'

Response:

{
  "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "scope": "credential:read:credential::*",
  "expiresIn": 3600,
  "tokenType": "Bearer"
}

Every credential returns the same scope value, credential:read:credential::*. It doesn't list the credential's permissions: Ripple Custody (Wallet) checks them on the server for each request, and a request outside them returns 403. expiresIn is the number of seconds until the token expires.

Authenticate each credential and export the tokens. The examples in this guide use $TOKEN for the Wallets credential, $CONTROLS_TOKEN for Controls, and $TX_TOKEN for Transactions:

export TOKEN="<Wallets accessToken>"
export CONTROLS_TOKEN="<Controls accessToken>"
export TX_TOKEN="<Transactions accessToken>"
Token validity and renewal

Tokens last 1 hour in Sandbox and Development, and 60 seconds in Production. Reuse a token until it expires, then request a new one. Always request tokens from /v2/credentials/oauth/token. The endpoint caches tokens on the server and returns the same token until it expires, so expiresIn gives the time remaining and is often less than 3600. You don't need to cache tokens yourself. Requesting tokens any other way uses a limited issuance quota and can cause authentication failures for your organization.

Step 3: Create a vault

A vault groups related wallets together. Create one to hold your first wallet.

curl -X POST https://api.sandbox.palisade.co/v2/vaults \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sandbox Testing",
    "description": "Vault for API getting-started walkthrough"
  }'

Response:

{
  "id": "019c8a2f-1234-7abc-9def-abcdef123456",
  "organizationId": "21c81319-5b83-45f9-b648-42055084af15",
  "createdBy": "a55df9e0-e14a-4410-a983-12afae46662f",
  "createdAt": "2025-05-08T10:00:00.000Z",
  "updatedAt": "2025-05-08T10:00:00.000Z",
  "name": "Sandbox Testing",
  "description": "Vault for API getting-started walkthrough"
}

Save the vault id — you need it for the next step.

export VAULT_ID="019c8a2f-1234-7abc-9def-abcdef123456"

Step 4: Create a wallet

Create an Ethereum wallet inside your vault. This example uses HSM (available in sandbox only) so you do not need a quorum.

curl -X POST https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ETH Sandbox Wallet",
    "description": "Ethereum wallet for API testing",
    "blockchain": "ETHEREUM",
    "keystore": "HSM"
  }'

The initial response has status: "CREATED" with an empty address. HSM wallets are provisioned within seconds. Poll the wallet until status changes to PROVISIONED:

curl -X GET "https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets/$WALLET_ID" \
  -H "Authorization: Bearer $TOKEN"

Response (provisioned):

{
  "id": "019c8a3b-5678-7def-1234-567890abcdef",
  "vaultId": "019c8a2f-1234-7abc-9def-abcdef123456",
  "organizationId": "...",
  "quorumId": "00000000-0000-0000-0000-000000000000",
  "createdBy": "a55df9e0-e14a-4410-a983-12afae46662f",
  "createdAt": "2025-05-08T10:01:00.000Z",
  "updatedAt": "2025-05-08T10:01:05.000Z",
  "name": "ETH Sandbox Wallet",
  "description": "Ethereum wallet for API testing",
  "address": "0x8dCd82eC41C41627D987830128198aff4A392D89",
  "publicKey": "04b05c60b4ac57b85c8d69a98409c5c5b4590d77...",
  "keystore": "HSM",
  "blockchain": "ETHEREUM",
  "settings": {
    "enabled": false,
    "rawSigningEnabled": false,
    "sweepingEnabled": false,
    "defaultFreezeEnabled": false
  },
  "status": "PROVISIONED",
  "reasons": [],
  "keyAlgorithm": "SECP256K1",
  "walletType": "WALLET_TYPE_UNSPECIFIED"
}

HSM wallets have no quorum, so quorumId is all zeros. Note that settings.enabled is false — the wallet is deposit-only until you explicitly enable outgoing transactions in a later step.

Save the wallet id and address:

export WALLET_ID="019c8a3b-5678-7def-1234-567890abcdef"
export WALLET_ADDRESS="0x8dCd82eC41C41627D987830128198aff4A392D89"
MPC wallets in production

For production wallets, set "keystore": "MPC" and include "quorumId": "YOUR_QUORUM_ID". MPC wallets require quorum device approval before moving to PROVISIONED.

Supported blockchains

Replace ETHEREUM with any supported blockchain. See Blockchains and tokens for the full list.

Step 5: Fund your wallet

Your wallet starts with a zero balance. Send testnet funds to the wallet address from a faucet or another wallet before proceeding.

For Ethereum testnet faucets, search for "Sepolia faucet" and send testnet ETH to your wallet address.

Verify the balance:

curl -X GET "https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets/$WALLET_ID/balances" \
  -H "Authorization: Bearer $TOKEN"

Response (abridged: each asset carries more fields than shown):

{
  "currencyCode": "USD",
  "aggregatedFiatValue": "0",
  "balances": [
    {
      "asset": {
        "id": "2::ETH",
        "symbol": "ETH",
        "name": "Ethereum",
        "blockchain": "ETHEREUM",
        "standard": "NATIVE",
        "decimals": 18
      },
      "balance": "0",
      "rate": "2450.12",
      "fiatValue": "0",
      "availableBalance": "0",
      "pendingBalance": "0",
      "frozenBalance": "0"
    }
  ],
  "pagination": null
}

Wait until balance shows a non-zero amount before continuing.

To filter, search, or page the balances this operation returns, see Filter, search, and page balances.

Step 6: Create a transaction policy

Wallets are deposit-only by default. Create a policy rule to permit outgoing transactions. This step requires a Controls credential with the policy:create and policy:read permissions.

Policy rules identify the asset by its registry ID. Read it from asset.id in the balances response in Step 5, for example 2::ETH.

This example limits each transaction to 10 ETH:

curl -X POST "https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets/$WALLET_ID/policy-rules" \
  -H "Authorization: Bearer $CONTROLS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger": "OUTBOUND_INITIATED",
    "assetId": "2::ETH",
    "definition": {
      "kind": "LIMIT",
      "limit": {
        "amount": {
          "threshold": "10",
          "operator": "GT",
          "aggregation": "SINGLE_TX"
        }
      }
    }
  }'

Response:

{
  "id": "c2655be7-795e-47d3-b4e9-2fc38b26febf",
  "organizationId": "...",
  "vaultId": "019c8a2f-1234-7abc-9def-abcdef123456",
  "walletId": "019c8a3b-5678-7def-1234-567890abcdef",
  "createdBy": "a55df9e0-e14a-4410-a983-12afae46662f",
  "trigger": "OUTBOUND_INITIATED",
  "assetId": "2::ETH",
  "matchers": [],
  "status": "RULE_CREATION_APPROVAL_PENDING",
  "active": false,
  "createdAt": "2025-05-08T10:05:00.000Z",
  "updatedAt": "2025-05-08T10:05:00.000Z",
  "definition": {
    "kind": "LIMIT",
    "limit": {
      "amount": {
        "threshold": "10",
        "operator": "GT",
        "aggregation": "SINGLE_TX"
      }
    }
  },
  "scope": "WALLET"
}

If the wallet already has an equivalent rule, the request returns 409 with reason PAL006.039.

Approval workflow

Every new policy rule starts with status: "RULE_CREATION_APPROVAL_PENDING" and active: false. If your organization has a Policy rules approval group, the rule stays in that status until an approver approves it in the console. If it doesn't, the rule becomes active without an approval.

Limit types

SINGLE_TX limits a single transaction. ROLLING limits the total over a time window (set "duration": "86400s" for 24 hours). TOTAL sets a lifetime cap. To hold large transactions for approvers or require a Travel Rule exchange, use a different rule kind. See Manage policy rules via the API and Policy reference.

Step 7: Add a destination address

Register an external destination in the address book before you can send to it. This step requires a Controls credential.

Create a counterparty

The details field requires a type (INDIVIDUAL, ORGANIZATION, or DAPP) and the corresponding detail object:

curl -X POST https://api.sandbox.palisade.co/v2/counterparties \
  -H "Authorization: Bearer $CONTROLS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Test Recipient",
    "description": "Sandbox test counterparty",
    "details": {
      "type": "INDIVIDUAL",
      "individual": {
        "firstName": "Test",
        "lastName": "Recipient"
      }
    }
  }'

Response:

{
  "id": "019d0623-86d1-7aef-9134-50e8d9cb3ee5",
  "organizationId": "...",
  "createdBy": "a55df9e0-e14a-4410-a983-12afae46662f",
  "createdAt": "2025-05-08T10:06:00.000Z",
  "updatedBy": "a55df9e0-e14a-4410-a983-12afae46662f",
  "updatedAt": "2025-05-08T10:06:00.000Z",
  "name": "Test Recipient",
  "description": "Sandbox test counterparty",
  "details": {
    "type": "INDIVIDUAL",
    "individual": {
      "firstName": "Test",
      "lastName": "Recipient"
    }
  },
  "addressCount": 0
}

Save the counterparty id:

export COUNTERPARTY_ID="019d0623-86d1-7aef-9134-50e8d9cb3ee5"
Counterparty types

Use ORGANIZATION with a legalName field for businesses, or DAPP for decentralized applications.

Add a blockchain address

curl -X POST "https://api.sandbox.palisade.co/v2/counterparties/$COUNTERPARTY_ID/addresses" \
  -H "Authorization: Bearer $CONTROLS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "termsAndConditionsAccepted": true,
    "details": {
      "type": "EXTERNAL",
      "externalAddress": {
        "address": "0xDESTINATION_ADDRESS",
        "name": "Recipient ETH Wallet",
        "blockchains": ["ETHEREUM"]
      }
    }
  }'

Replace 0xDESTINATION_ADDRESS with a valid Ethereum address. The API validates the address against the selected blockchains and returns 400 with reason PAL013.015 if it isn't valid for one of them.

Response:

{
  "addressId": "019d0624-b5d7-77cf-8eee-b72ceac0f71e",
  "counterpartyId": "019d0623-86d1-7aef-9134-50e8d9cb3ee5",
  "organizationId": "...",
  "createdBy": "a55df9e0-e14a-4410-a983-12afae46662f",
  "createdAt": "2025-05-08T10:07:00.000Z",
  "updatedBy": "a55df9e0-e14a-4410-a983-12afae46662f",
  "updatedAt": "2025-05-08T10:07:00.000Z",
  "custodian": {
    "name": "",
    "dId": ""
  },
  "termsAndConditionsAccepted": true,
  "details": {
    "type": "EXTERNAL",
    "externalAddress": {
      "address": "0xDESTINATION_ADDRESS",
      "name": "Recipient ETH Wallet",
      "blockchains": ["ETHEREUM"]
    }
  },
  "status": "CREATION_APPROVAL_PENDING",
  "active": false
}
Approval workflow

Every new address starts with status: "CREATION_APPROVAL_PENDING". If your organization has approval groups configured, it stays in that status until an approver approves it. After approval, or immediately if no approval groups are configured, the address passes a compliance check before it becomes active.

Step 8: Enable outgoing transactions

Enable outgoing transactions on the wallet. This step requires a Wallets credential. The request replaces all four wallet settings, so send the current value of each one: a setting you omit is set to false.

curl -X PUT "https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets/$WALLET_ID/settings" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "settings": {
      "enabled": true,
      "rawSigningEnabled": false,
      "sweepingEnabled": false,
      "defaultFreezeEnabled": false
    }
  }'

The response is the full wallet object with settings.enabled now set to true.

Step 9: Send a transaction

Send 0.01 ETH to the approved destination address. This step requires a Transactions credential.

curl -X POST "https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets/$WALLET_ID/transactions/transfer" \
  -H "Authorization: Bearer $TX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "destinationAddress": "0xDESTINATION_ADDRESS",
    "symbol": "ETH",
    "qty": "0.01"
  }'

Replace 0xDESTINATION_ADDRESS with the address you registered in Step 7.

Response (abridged: the transaction object has many more fields, such as origin, destination, feeAsset, freezeInfo, problems, and history):

{
  "id": "019c94f1-cc8c-79b4-9c0f-bc7edaabc267",
  "walletId": "...",
  "vaultId": "...",
  "organizationId": "...",
  "createdAt": "2025-05-08T10:20:00.000Z",
  "status": "REQUESTED",
  "action": "PALISADE_TRANSFER",
  "asset": {
    "symbol": "ETH"
  },
  "blockchain": "ETHEREUM",
  "destinationAddress": "0xDESTINATION_ADDRESS",
  "originAddress": "0x8dCd82eC41C41627D987830128198aff4A392D89",
  "qty": "0.01"
}

Save the transaction id:

export TX_ID="019c94f1-cc8c-79b4-9c0f-bc7edaabc267"

Step 10: Track the transaction

Poll the transaction until it reaches a terminal status (CONFIRMED, REJECTED, or FAILED). Use a Transactions credential:

curl -X GET "https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets/$WALLET_ID/transactions/$TX_ID" \
  -H "Authorization: Bearer $TX_TOKEN"

The status field progresses through the lifecycle until it reaches CONFIRMED. If the transaction ends in REJECTED or FAILED, its problems array says why. For example, a transfer from a wallet whose only limit rules are still pending approval is rejected with reason PAL006.005. See Transaction problems.

The transaction moves through these statuses:

REQUESTED → POLICY_CHECK_PENDING → POLICY_CHECK_PASSED → COMPILATION_PENDING → COMPILED → SIGNATURE_PENDING → SIGNED → PUBLISH_PENDING → PUBLISHED → CONFIRMATION_PENDING → CONFIRMED

Approvals happen during the policy check

If the transaction needs approvals, it collects them while it's in POLICY_CHECK_PENDING. POLICY_CHECK_PASSED means the transaction passed both policy checks and approvals.

Use webhooks instead of polling

For production integrations, set up webhooks to receive real-time status updates instead of polling.

Best practices

  • Store API credentials securely, and never expose them in client-side code.
  • Use the sandbox environment for testing before you move to production.
  • Implement error handling for all API calls.
  • Set up webhooks for real-time transaction status updates.

Next steps

For API support, contact Ripple Support.