Skip to content

Manage policy rules via the API

This guide shows you how to create, list, get, and delete a wallet's policy rules through the Ripple Custody (Wallet) API. A policy rule has a kind: LIMIT rules let a wallet send up to an amount, APPROVAL rules hold large transfers for named approvers, and TRAVEL_RULE rules require a Travel Rule data exchange. A wallet can't send transfers of an asset until a LIMIT rule applies to it. A few operations, such as sweeps and passkey transfers, skip limit evaluation.

Availability

Travel Rule policies are available in sandbox. They aren't available in production yet.

For how the rule kinds work together, see Policies. To manage rules in the console instead, see Manage policies via the console.

Policy actions

ActionConsole roleConsole procedureAPI procedure
Create a limit, approval, or Travel Rule ruleOwner, AdministratorCreate a policyCreate a policy rule
Check rule statusAll except ViewerView policy statusList a wallet's policy rules
Get a policy ruleAll except ViewerOpen the wallet's Policy tab.Get a policy rule
Change a ruleOwner, AdministratorModify a policyChange a policy rule
Delete a ruleOwner, AdministratorDelete a policyDelete a policy rule
Manage wallet limits--Use the wallet limits endpoints

Permissions: In the console, owners and administrators can create and delete policy rules. Owners, administrators, proposers, and approvers can approve them, and every role except viewer can view them. See User roles and permissions. The API procedures need an API credential of type CONTROLS. See Prerequisites.

Prerequisites

  • An API credential of type CONTROLS. Grant it the policy:create, policy:read, and policy:delete permissions at organization, vault, or wallet scope. The older wallet limits endpoints use the separate keylimit:create, keylimit:read, and keylimit:delete permissions. Without the policy:* permissions, the policy rules endpoints return HTTP 403 with PAL000.002, even when the wallet limits endpoints work. Ripple Custody (Wallet) validates the request body before it checks permissions, so a malformed create request from a credential that lacks policy:create returns HTTP 400 with PAL000.020 instead. See API credentials overview and Manage API credentials.
  • An access token. Exchange your credential's clientId and clientSecret for a token. See Getting started with the API.
  • A vault and wallet. Policy rules belong to one wallet. See Create your first vault and Create your first wallet.
  • The asset's ID. Policy rules identify the asset by its registry ID, for example 3::XRP. Read asset.id from the wallet's balances (GET /v2/vaults/{vaultId}/wallets/{walletId}/balances, which needs a WALLETS credential).

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

Policy rule approvals

If your organization has a Policy rules approval group, a rule that you create or delete through the API waits for approvers before the change takes effect. A new rule has status RULE_CREATION_APPROVAL_PENDING and active: false until approvers approve it. Approvers act on the request in the console. Without a Policy rules approval group, the create response still shows RULE_CREATION_APPROVAL_PENDING and active: false, because Ripple Custody (Wallet) approves the rule automatically only after it returns the response. Get the rule until its status is RULE_ENABLED before you rely on it. Ripple Custody (Wallet) doesn't enforce the rule until then.

Create a policy rule via the API

API reference: Create a policy rule

Required permission: policy:create

Send a POST request to /v2/vaults/{vaultId}/wallets/{walletId}/policy-rules. Set definition.kind to the rule kind, and include only the matching object: limit, approval, or travelRule.

This example creates a LIMIT rule that caps payments to any counterparty at 10,000 XRP in a rolling 24-hour window:

curl -X POST https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets/$WALLET_ID/policy-rules \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger": "OUTBOUND_INITIATED",
    "assetId": "3::XRP",
    "matchers": [
      { "type": "ALL_COUNTERPARTIES", "values": [] }
    ],
    "definition": {
      "kind": "LIMIT",
      "limit": {
        "amount": {
          "threshold": "10000",
          "operator": "GT",
          "aggregation": "ROLLING",
          "duration": "86400s"
        }
      }
    }
  }'

For APPROVAL and TRAVEL_RULE rules, use the same request with a different definition. See Create a step-up approval rule via the API and Create a Travel Rule policy rule via the API.

Request body

FieldTypeRequiredDescription
triggerstringYesOUTBOUND_INITIATED for transactions the wallet sends, or INBOUND_DETECTED for deposits into the wallet. Only TRAVEL_RULE rules support INBOUND_DETECTED.
assetIdstringConditionalThe asset's registry ID. LIMIT and APPROVAL rules require it. To apply a TRAVEL_RULE rule with no amount condition to all assets, leave it out. The asset's blockchain must match the wallet's blockchain, except that an EVM wallet accepts an asset on another EVM chain.
matchersarrayNoFilters that narrow which transactions the rule applies to. Send each matcher as { "type": "...", "values": ["..."] }. APPROVAL rules don't support matchers. See Matchers.
definitionobjectYesThe rule kind and its settings. Set definition.kind to LIMIT, APPROVAL, or TRAVEL_RULE, and include only the matching object: limit, approval, or travelRule.

Amount condition

LIMIT, APPROVAL, and TRAVEL_RULE rules use the same amount object, for example definition.limit.amount:

FieldTypeDescription
thresholdstringDecimal amount in the asset's units, for example "10000" or "0.5". "0" is valid. A negative amount returns PAL000.020.
operatorstringGT (greater than). The schema also lists GTE, but Ripple Custody (Wallet) doesn't support it yet and rejects it.
aggregationstringSINGLE_TX compares each transaction on its own. ROLLING compares the total of the rule's asset that the wallet sent during the duration window, counting only transactions that match the rule's matchers. TOTAL does the same over all time. Pending transactions count. Rejected and failed ones don't.
durationstringThe window length in seconds, for example "86400s" for 24 hours. Must be less than one year. Use it only with ROLLING, which requires it. A ROLLING rule without it, or another aggregation with it, returns PAL006.040. A window of one year or more returns PAL000.020.
KindSupported aggregation values
LIMITSINGLE_TX, ROLLING, TOTAL
APPROVALSINGLE_TX
TRAVEL_RULESINGLE_TX
CHAIN_ID matchers reject natively integrated chains

A CHAIN_ID matcher authorizes cross-chain raw signing for an EVM chain that Ripple Custody (Wallet) doesn't natively integrate. If the value is the chain ID of a natively integrated chain, for example Ethereum mainnet 1, Ripple Custody (Wallet) rejects the request. It checks the chain IDs of the environment you call, and skips the check if it can't look them up. The check applies to every wallet, including non-EVM wallets, but a CHAIN_ID matcher on a non-EVM wallet never matches a transaction. Add CHAIN_ID matchers only to EVM wallets.

Response

The response is the new rule. With a Policy rules approval group, it has status RULE_CREATION_APPROVAL_PENDING and active: false:

{
  "id": "0199a1c2-5d3e-7a10-9b2f-4c8e1d6f0a21",
  "organizationId": "a9a1de05-0b19-4fca-8597-1199353bd569",
  "vaultId": "019be892-2834-7435-be18-bc13ac4c73ce",
  "walletId": "019bec51-ab18-7284-907a-dd7747277116",
  "createdBy": "8617b5b5-dfcf-4ffc-9f46-f7cec35a88bd",
  "trigger": "OUTBOUND_INITIATED",
  "assetId": "3::XRP",
  "matchers": [
    { "type": "ALL_COUNTERPARTIES", "value": "", "values": [] }
  ],
  "status": "RULE_CREATION_APPROVAL_PENDING",
  "active": false,
  "createdAt": "2026-09-24T12:00:00Z",
  "updatedAt": "2026-09-24T12:00:00Z",
  "definition": {
    "kind": "LIMIT",
    "limit": {
      "amount": {
        "threshold": "10000",
        "operator": "GT",
        "aggregation": "ROLLING",
        "duration": "86400s"
      }
    }
  },
  "scope": "WALLET"
}

The response echoes each matcher with both values and the deprecated value string, and includes duration only for ROLLING rules. scope is WALLET for rules that you create. For each status, see Rule statuses. For error codes, see Policy reference.

List a wallet's policy rules via the API

API reference: List policy rules, List policy rules with a request body

Required permission: policy:read

curl "https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets/$WALLET_ID/policy-rules?filter.kind=LIMIT&filter.active.eq=true" \
  -H "Authorization: Bearer $ACCESS_TOKEN"
  • Paging: pagination.pageSize (default 50, maximum 1,000) and pagination.pageToken. Pass filter.nextPageToken from the response as the next pagination.pageToken. The top-level pageSize and pageToken also work on their own, but a request that sends both returns HTTP 400, so use the pagination.* form.
  • Filters: filter.kind, filter.trigger, and filter.status take one enum value, for example filter.status=RULE_ENABLED. The list hides RULE_DELETED rules unless you set filter.status. filter.id, filter.vaultId, filter.walletId, filter.assetId, filter.active, filter.createdAt, and filter.updatedAt take an operator, for example filter.assetId.eq=3::XRP or filter.createdAt.after=2026-09-01T00:00:00Z. eq is case-sensitive.
  • Search: search matches the rule ID, vault ID, wallet ID, and asset ID, without regard to case.
  • Sorting: pagination.orderBy takes id, type (the rule's kind), trigger, asset_id, status, active, created_at, or updated_at, and pagination.order takes SORT_ORDER_ASC or SORT_ORDER_DESC. Field names are in snake case: createdAt returns HTTP 400. By default, the newest rule comes first.

To send the filters in a JSON body instead, use POST /v2/vaults/{vaultId}/wallets/{walletId}/policy-rules:list.

The response holds the rules in a policyRules array, with each rule in the same shape as the create response. This example is abridged:

{
  "filter": {
    "nextPageToken": "cGFnZV9zaXplPTEwJnBhZ2VfdG9rZW49MjA=",
    "total": 3
  },
  "policyRules": [
    {
      "id": "0199a1c2-5d3e-7a10-9b2f-4c8e1d6f0a21",
      "vaultId": "019be892-2834-7435-be18-bc13ac4c73ce",
      "walletId": "019bec51-ab18-7284-907a-dd7747277116",
      "trigger": "OUTBOUND_INITIATED",
      "assetId": "3::XRP",
      "status": "RULE_ENABLED",
      "active": true,
      "definition": {
        "kind": "LIMIT",
        "limit": {
          "amount": {
            "threshold": "10000",
            "operator": "GT",
            "aggregation": "ROLLING",
            "duration": "86400s"
          }
        }
      }
    }
  ]
}

Get a policy rule via the API

API reference: Get a policy rule

Required permission: policy:read

curl https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets/$WALLET_ID/policy-rules/$RULE_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN"

The response is the rule, in the same shape as the create response. A rule belongs to one wallet. If the rule doesn't exist on the wallet in the path, the API returns HTTP 404 with PAL006.041, even when the rule exists on another wallet.

Change a policy rule via the API

There is no update operation, so you can't edit a rule. To change one, delete the old rule and wait until the deletion completes. For an enabled rule, that's when a get request for it returns HTTP 404 with PAL006.041. A rule that you delete while it's RULE_REJECTED or RULE_ERROR shows RULE_DELETED instead. Then create the new rule. A replacement that differs from the old rule only in amount or window counts as a duplicate while the old rule exists, and the API returns HTTP 409 with PAL006.039. A rule counts until it's deleted, whatever its state. Until the new rule takes effect, Ripple Custody (Wallet) rejects transfers of the asset that no other LIMIT rule covers, with PAL006.005. You can create the new rule first only if it differs from the old one in its matchers, asset, aggregation, or trigger, or, for an APPROVAL rule, in its threshold.

Delete a policy rule via the API

API reference: Delete a policy rule

Required permission: policy:delete

curl -X DELETE https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets/$WALLET_ID/policy-rules/$RULE_ID \
  -H "Authorization: Bearer $ACCESS_TOKEN"

A successful request returns an empty JSON object. With a Policy rules approval group, the rule moves to RULE_DELETION_APPROVAL_PENDING and keeps enforcing until approvers approve the deletion. A rule in RULE_REJECTED or RULE_ERROR status skips approval and moves straight to RULE_DELETED. For every status, see Rule statuses.

You can't delete a rule that is waiting for creation or deletion approval: the API returns HTTP 400 with PAL006.016. A rule ID that doesn't exist on the wallet returns HTTP 404 with PAL006.041.

Use the wallet limits endpoints via the API

API reference: Create a wallet limit policy, List wallet limit policies, List wallet limit policies with a request body, Get a wallet limit policy, Delete a wallet limit policy, List organization wallet limits, List organization wallet limits with a request body

The wallet limits endpoints predate the policy rules API and keep working. They manage LIMIT rules and use the keylimit:* permissions. Use only LIMIT rule IDs with the endpoints that get or delete a limit by ID.

Both APIs read and write the same LIMIT rules. A wallet limit appears in the policy rules API as a LIMIT rule with the same ID, and a LIMIT rule that you create with the policy rules API appears in the wallet limits endpoints. Duplicate checks span both APIs: a duplicate returns PAL006.039 from the policy rules API and PAL006.023 from the wallet limits endpoints. The two APIs show the same rule in different shapes:

Wallet limits fieldPolicy rules field
limitType: PER_TX, ROLLING_DURATION, CONSTANTdefinition.limit.amount.aggregation: SINGLE_TX, ROLLING, TOTAL
limitQtydefinition.limit.amount.threshold, with operator GT
duration, "0s" when unuseddefinition.limit.amount.duration, present only for ROLLING
asset objectassetId
status: LIMIT_* valuesstatus: the matching RULE_* value, for example LIMIT_CREATION_APPROVAL_PENDING and RULE_CREATION_APPROVAL_PENDING
No equivalenttrigger (always OUTBOUND_INITIATED) and scope

The wallet limits endpoints are PUT, GET, and POST …/limits:list on /v2/vaults/{vaultId}/wallets/{walletId}/policy-rules/limits, GET and DELETE on /v2/vaults/{vaultId}/wallets/{walletId}/policy-rules/limits/{id}, and GET /v2/policy-rules/limits or POST /v2/policy-rules/limits:list across the organization.

To create a wallet limit, send limitQty, limitType, and symbol. Set limitType to PER_TX, CONSTANT, or ROLLING_DURATION, and set duration for ROLLING_DURATION. The window must be less than one year. Always set duration for a rolling limit: the API doesn't require it, and creates a ROLLING_DURATION limit without one with a duration of "0s". For a token, also set contract to the token contract or issuing address. blockchain defaults to the wallet's blockchain.

curl -X PUT https://api.sandbox.palisade.co/v2/vaults/$VAULT_ID/wallets/$WALLET_ID/policy-rules/limits \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "limitQty": "200.00",
    "limitType": "ROLLING_DURATION",
    "duration": "86400s",
    "symbol": "XRP",
    "matchers": [
      {
        "type": "COUNTERPARTY_ID",
        "values": ["ce4918bf-a199-4ce2-85a3-d0d296855384"]
      }
    ]
  }'

The response is the new wallet limit, with the asset as an asset object (abridged in this example). Ripple Custody (Wallet) normalizes limitQty, so "200.00" comes back as "200", and echoes each matcher with both value and values:

{
  "id": "f1dc7bc7-107c-40df-b254-50f2e041e575",
  "createdBy": "8617b5b5-dfcf-4ffc-9f46-f7cec35a88bd",
  "organizationId": "a9a1de05-0b19-4fca-8597-1199353bd569",
  "walletId": "019bec51-ab18-7284-907a-dd7747277116",
  "vaultId": "019be892-2834-7435-be18-bc13ac4c73ce",
  "limitQty": "200",
  "duration": "86400s",
  "limitType": "ROLLING_DURATION",
  "createdAt": "2026-09-24T12:00:00Z",
  "updatedAt": "2026-09-24T12:00:00Z",
  "asset": {
    "id": "3::XRP",
    "standard": "NATIVE",
    "symbol": "XRP",
    "name": "XRP",
    "blockchain": "XRP_LEDGER",
    "decimals": 6,
    "contract": ""
  },
  "matchers": [
    {
      "type": "COUNTERPARTY_ID",
      "value": "ce4918bf-a199-4ce2-85a3-d0d296855384",
      "values": ["ce4918bf-a199-4ce2-85a3-d0d296855384"]
    }
  ],
  "status": "LIMIT_CREATION_APPROVAL_PENDING",
  "active": false
}

To audit limits across every vault and wallet, use the organization-level list. It supports pageSize, pageToken, search (wallet limit ID, vault ID, wallet ID, and asset symbol), filter.limitType, filter.status, and operator filters such as filter.walletId.eq, filter.assetSymbol.eq, and filter.blockchain.eq. The response holds the limits in a walletLimits array. This endpoint needs keylimit:read at organization scope, so a credential that's scoped to a vault or wallet gets HTTP 403. See Manage API credentials.

curl "https://api.sandbox.palisade.co/v2/policy-rules/limits?filter.status=LIMIT_ENABLED&filter.assetSymbol.eq=XRP" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

A wallet limit has the same lifecycle as the matching policy rule. For the full status reference, see Rule statuses. The wallet limits endpoints use these status values:

Wallet limits statusPolicy rules statusMeaning
LIMIT_CREATEDRULE_CREATEDRipple Custody (Wallet) stored the limit and is about to request approval. This status is short-lived.
LIMIT_CREATION_APPROVAL_PENDINGRULE_CREATION_APPROVAL_PENDINGThe limit is waiting for approvers to approve it.
LIMIT_CREATION_APPROVAL_COMPLETERULE_CREATION_APPROVAL_COMPLETERipple Custody (Wallet) recorded the approval decision. The limit becomes LIMIT_ENABLED if approvers approved it, or LIMIT_REJECTED if not. This status is short-lived.
LIMIT_ENABLEDRULE_ENABLEDThe limit is active and enforcing.
LIMIT_REJECTEDRULE_REJECTEDApprovers rejected the limit. You can't enable it again, and it still blocks an equivalent limit until you delete it.
LIMIT_DELETION_APPROVAL_PENDINGRULE_DELETION_APPROVAL_PENDINGThe deletion is waiting for approvers to approve it. The limit keeps enforcing.
LIMIT_DELETION_APPROVAL_COMPLETERULE_DELETION_APPROVAL_COMPLETERipple Custody (Wallet) recorded the deletion decision. The limit becomes LIMIT_DELETED if approvers approved the deletion, or returns to LIMIT_ENABLED if not. This status is short-lived.
LIMIT_DELETEDRULE_DELETEDRipple Custody (Wallet) deleted the limit. A limit that you delete while it's LIMIT_REJECTED or LIMIT_ERROR moves straight here, with no approval.
LIMIT_ERRORRULE_ERRORRipple Custody (Wallet) couldn't process the limit. Delete it and create it again, or contact Ripple Support.

For error codes, including PAL006.023 from these endpoints, see Policy reference.