Workflow guide

Withdraw funds

The safest payout flow is quote first, create once, then track the non-terminal states until a final tx hash or failure reason exists.

Recommended payout flow

  1. Quote with the exact amount, token, chain, and network you intend to send.
  2. Inspect whether a sweep is needed and whether the payout is actually feasible.
  3. Create the withdrawal once, using dashboard step-up or a dedicated payout API key, with `Idempotency-Key` and your own `clientReference`.
  4. Track the payout until it reaches `completed`, `failed`, or `cancelled`.

Programmatic withdrawals

For automation, create a payout API key first. The key can use an allowlist or dynamic destinations, but token, network, per-withdrawal, daily, and optional IP policy still apply.

Requested amount vs payout amount

PayChainHQ withdrawal fees are deducted from the requested amount. Native-chain gas may reduce the final send amount even further for native-asset payouts.

Endpoints involved

POST/api/v1/businesses/:id/payout-api-keysDashboard UI onlySandbox + live

Create payout API key

Create a dedicated payout API key with allowlisted or dynamic destinations within configured limits.

When to use it

Use when a backend needs programmatic withdrawals after you have defined destination policy, token, network, spend, and optional IP guardrails.

FieldLocationRequiredDescriptionExample / default
idpathYesBusiness ID.-
namebodyYesOperator label for this payout key.-
destinationPolicybodyNo`allowlist` by default. Set to `any` to allow withdrawals to any valid destination while still enforcing token, network, spend, and optional IP limits.-
destinationsbodyNoAllowed withdrawal destinations, each with `chain`, `address`, and optional `label`. Required when `destinationPolicy` is `allowlist`.-
limitsbodyYesAllowed `chain`, `networkId`, `token`, `maxAmount` per withdrawal, and rolling `dailyAmount`.-
ipAllowlistbodyNoOptional IPv4 addresses or CIDR blocks that may use this key.-
expiresInbodyNoOptional key lifetime in days, max 365.-

Cautions

  • Payout keys cannot create dashboard sessions, manage API keys, or bypass the configured payout policy.
  • `destinationPolicy: any` increases payout-key blast radius; use lower limits, shorter expiry, and IP allowlists when possible.
  • The withdrawal request still needs an `Idempotency-Key`; policy approval is not a settlement guarantee.
  • Rotate payout keys separately from standard API keys because the blast radius and alerting profile are different.

Common errors

  • 401: Missing or invalid API key.
  • 403: The caller is missing the required permission or auth mode for this action.
  • 400: Payload shape, query params, or business-state validation failed.

Sample request body

json
{
  "name": "Vendor payouts",
  "expiresIn": 30,
  "destinationPolicy": "allowlist",
  "destinations": [
    {
      "chain": "sol",
      "address": "8gRLe6GiQx2jo9mQFcbwy34u2W4zD2rN5q4mmD8iP2Mp",
      "label": "Solana operations wallet"
    },
    {
      "chain": "eth",
      "address": "0x5fBDB2315678afecb367f032d93F642f64180aa3",
      "label": "Base operations wallet"
    }
  ],
  "limits": [
    {
      "chain": "sol",
      "networkId": "sol-mainnet",
      "token": "USDC",
      "maxAmount": "1000",
      "dailyAmount": "5000"
    },
    {
      "chain": "eth",
      "networkId": "base-mainnet",
      "token": "USDC",
      "maxAmount": "1000",
      "dailyAmount": "5000"
    }
  ],
  "ipAllowlist": ["203.0.113.10/32"]
}

Sample response

json
{
  "id": "key_payout_123",
  "name": "Vendor payouts",
  "kind": "payout",
  "apiKey": "biz_live_payout_1234567890abcdef",
  "permissions": ["payouts:create", "payouts:read"],
  "expiresAt": "2026-04-11T15:35:00.000Z",
  "createdAt": "2026-03-12T15:35:00.000Z",
  "policy": {
    "destinationPolicy": "allowlist",
    "destinations": [
      {
        "chain": "sol",
        "address": "8gRLe6GiQx2jo9mQFcbwy34u2W4zD2rN5q4mmD8iP2Mp",
        "label": "Solana operations wallet"
      }
    ],
    "allowedChains": ["sol", "eth"],
    "allowedNetworkIds": ["sol-mainnet", "base-mainnet"],
    "allowedTokens": ["USDC"],
    "limits": [
      {
        "chain": "sol",
        "networkId": "sol-mainnet",
        "token": "USDC",
        "maxAmount": "1000",
        "dailyAmount": "5000"
      }
    ],
    "ipAllowlist": ["203.0.113.10/32"]
  },
  "warning": "Store this payout API key securely. It can create withdrawals only within the configured policy and cannot be recovered."
}

cURL example

bash
curl -X POST "https://api.paychainhq.io/api/v1/businesses/biz_123/payout-api-keys" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Vendor payouts",
  "expiresIn": 30,
  "destinationPolicy": "allowlist",
  "destinations": [
    {
      "chain": "sol",
      "address": "8gRLe6GiQx2jo9mQFcbwy34u2W4zD2rN5q4mmD8iP2Mp",
      "label": "Solana operations wallet"
    },
    {
      "chain": "eth",
      "address": "0x5fBDB2315678afecb367f032d93F642f64180aa3",
      "label": "Base operations wallet"
    }
  ],
  "limits": [
    {
      "chain": "sol",
      "networkId": "sol-mainnet",
      "token": "USDC",
      "maxAmount": "1000",
      "dailyAmount": "5000"
    },
    {
      "chain": "eth",
      "networkId": "base-mainnet",
      "token": "USDC",
      "maxAmount": "1000",
      "dailyAmount": "5000"
    }
  ],
  "ipAllowlist": ["203.0.113.10/32"]
}'

Node.js example

ts
const response = await fetch('https://api.paychainhq.io/api/v1/businesses/biz_123/payout-api-keys', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "name": "Vendor payouts",
  "expiresIn": 30,
  "destinationPolicy": "allowlist",
  "destinations": [
    {
      "chain": "sol",
      "address": "8gRLe6GiQx2jo9mQFcbwy34u2W4zD2rN5q4mmD8iP2Mp",
      "label": "Solana operations wallet"
    },
    {
      "chain": "eth",
      "address": "0x5fBDB2315678afecb367f032d93F642f64180aa3",
      "label": "Base operations wallet"
    }
  ],
  "limits": [
    {
      "chain": "sol",
      "networkId": "sol-mainnet",
      "token": "USDC",
      "maxAmount": "1000",
      "dailyAmount": "5000"
    },
    {
      "chain": "eth",
      "networkId": "base-mainnet",
      "token": "USDC",
      "maxAmount": "1000",
      "dailyAmount": "5000"
    }
  ],
  "ipAllowlist": ["203.0.113.10/32"]
}),
});
const payload = await response.json();
console.log(payload);
GET/api/v1/businesses/:id/withdrawals/quotex-api-keySandbox + live

Get withdrawal quote

Estimate treasury availability, sweep requirements, and payout cost before creating a withdrawal.

When to use it

Always quote before presenting a payout option to operators or customers.

FieldLocationRequiredDescriptionExample / default
idpathYesBusiness ID.-
amountqueryYesRequested amount in token display units.-
tokenqueryYesToken symbol.-
chainqueryYesChain family.-
networkIdqueryNoSpecific network.-
forceSweepAllqueryNoForce sweep all eligible balances before quoting.-

Flags and defaults

  • forceSweepAll (default: false): Include dust / low-value balances in the sweep plan.

Cautions

  • Quote availability is still subject to confirmation, treasury freshness, and runtime network policy at execution time.
  • Force sweep does not guarantee fulfillment if net sweepable-to-treasury value is still insufficient.

Common errors

  • 401: Missing or invalid API key.
  • 400: Payload shape, query params, or business-state validation failed.

Sample response

json
{
  "requestedAmountUSD": 500,
  "token": "USDC",
  "chain": "eth",
  "networkId": "base-mainnet",
  "treasuryBalanceUSD": 220,
  "totalAvailableUSD": 760.5,
  "addressesWithFunds": 8,
  "sweepNeeded": true,
  "recommended": "exact",
  "message": "Withdrawal feasible. Will sweep 3 address(es) first.",
  "options": [
    {
      "type": "exact_withdrawal",
      "label": "Withdraw Exact Amount",
      "payoutUSD": 500,
      "sweepCount": 3,
      "totalCostUSD": 2.34,
      "feePercent": 0.47,
      "available": true
    }
  ]
}

cURL example

bash
curl -X GET "https://api.paychainhq.io/api/v1/businesses/biz_123/withdrawals/quote" \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_your_business_key"

Node.js example

ts
const response = await fetch('https://api.paychainhq.io/api/v1/businesses/biz_123/withdrawals/quote', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'pk_live_your_business_key',
  },
});
const payload = await response.json();
console.log(payload);
POST/api/v1/businesses/:id/withdrawalsx-api-keySandbox + live

Create withdrawal

Queue a treasury withdrawal with dashboard step-up or a policy-controlled payout API key.

When to use it

Use only after quoting and after your operator or automation is ready to accept async processing.

Idempotency: required

FieldLocationRequiredDescriptionExample / default
idpathYesBusiness ID.-
amountbodyYesRequested amount in token display units.-
tokenbodyYesToken symbol.-
chainbodyYesChain family.-
networkIdbodyNoSpecific network.-
destinationbodyYesDestination address.-
clientReferencebodyNoYour payout reference for idempotent reconciliation.-
referencebodyNoHuman-readable payout reference.-
forceSweepAllbodyNoForce sweeping all eligible funds before payout.-

Flags and defaults

  • forceSweepAll (default: false): Force sweep every eligible non-treasury address before payout.

Cautions

  • Send a stable `Idempotency-Key` per intended payout and reuse it for retries until you receive or can fetch the created withdrawal.
  • Standard API keys cannot create withdrawals. Programmatic withdrawals require a dedicated payout API key.
  • Payout API keys can withdraw only to configured destinations within their token, network, amount, daily, and IP policy.
  • Requested amount and payout amount can differ because PayChainHQ withdrawal fees are deducted from the requested amount.
  • For native assets, network gas can reduce the send amount further at broadcast time.

Common errors

  • 401: Missing or invalid API key.
  • 403 HIGH_RISK_API_KEY_SCOPE_DISABLED: A standard API key attempted a protected operation that now requires dashboard session auth or a dedicated payout or contract execution API key.
  • 403 PAYOUT_POLICY_REJECTED: A payout API key withdrawal failed destination policy, token, network, amount, daily cap, or IP allowlist policy.
  • 400: Payload shape, query params, or business-state validation failed.
  • Cannot fulfill withdrawal: Net treasury + sweepable balance cannot satisfy the requested payout.

Sample request body

json
{
  "amount": "500",
  "token": "USDC",
  "chain": "eth",
  "networkId": "base-mainnet",
  "destination": "0x5fBDB2315678afecb367f032d93F642f64180aa3",
  "clientReference": "vendor-payout-2026-03-0031",
  "reference": "Vendor March settlement",
  "metadata": {
    "vendorId": "ven_29831",
    "batch": "march-settlement"
  },
  "forceSweepAll": true
}

Sample response

json
{
  "id": "wdr_123",
  "status": "pending",
  "state": "queued",
  "requestedAmount": {
    "raw": "500000000",
    "decimals": 6,
    "display": "500",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "payoutAmount": {
    "raw": "498800000",
    "decimals": 6,
    "display": "498.8",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "withdrawalFee": {
    "raw": "1200000",
    "decimals": 6,
    "display": "1.2",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "destination": "0x5fBDB2315678afecb367f032d93F642f64180aa3"
}

cURL example

bash
curl -X POST "https://api.paychainhq.io/api/v1/businesses/biz_123/withdrawals" \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_your_business_key" \
  -H "Idempotency-Key: example-request-001" \
  -d '{
  "amount": "500",
  "token": "USDC",
  "chain": "eth",
  "networkId": "base-mainnet",
  "destination": "0x5fBDB2315678afecb367f032d93F642f64180aa3",
  "clientReference": "vendor-payout-2026-03-0031",
  "reference": "Vendor March settlement",
  "metadata": {
    "vendorId": "ven_29831",
    "batch": "march-settlement"
  },
  "forceSweepAll": true
}'

Node.js example

ts
const response = await fetch('https://api.paychainhq.io/api/v1/businesses/biz_123/withdrawals', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'pk_live_your_business_key',
    'Idempotency-Key': 'example-request-001',
  },
  body: JSON.stringify({
  "amount": "500",
  "token": "USDC",
  "chain": "eth",
  "networkId": "base-mainnet",
  "destination": "0x5fBDB2315678afecb367f032d93F642f64180aa3",
  "clientReference": "vendor-payout-2026-03-0031",
  "reference": "Vendor March settlement",
  "metadata": {
    "vendorId": "ven_29831",
    "batch": "march-settlement"
  },
  "forceSweepAll": true
}),
});
const payload = await response.json();
console.log(payload);
GET/api/v1/businesses/:id/withdrawalsx-api-keySandbox + live

List withdrawals

List withdrawal attempts and their current processing states.

When to use it

Use for operational dashboards, payout polling, and settlement exports.

FieldLocationRequiredDescriptionExample / default
idpathYesBusiness ID.-
statusqueryNoFilter by withdrawal status.-
pagequeryNoPage number.1
limitqueryNoPage size.20

Cautions

  • Treat `pending`, `sweeping`, `processing`, and `retrying` as non-terminal. Only `completed`, `failed`, and `cancelled` are terminal.
  • Payout API keys can list only withdrawals created by that same payout key.

Common errors

  • 401: Missing or invalid API key.

Sample response

json
{
  "data": [
    {
      "id": "wdr_123",
      "status": "processing",
      "processingStep": "broadcast_withdrawal",
      "token": "USDC",
      "chain": "eth",
      "networkId": "base-mainnet",
      "destination": "0x5fBDB2315678afecb367f032d93F642f64180aa3"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "totalPages": 1,
    "hasMore": false
  }
}

cURL example

bash
curl -X GET "https://api.paychainhq.io/api/v1/businesses/biz_123/withdrawals?page=1&limit=20" \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_your_business_key"

Node.js example

ts
const response = await fetch('https://api.paychainhq.io/api/v1/businesses/biz_123/withdrawals?page=1&limit=20', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'pk_live_your_business_key',
  },
});
const payload = await response.json();
console.log(payload);
GET/api/v1/businesses/:id/withdrawals/:withdrawalIdx-api-keySandbox + live

Get withdrawal detail

Return one withdrawal with fee, payout, retry, and processing metadata.

When to use it

Use after webhook receipt or when drilling into a failed or retrying payout.

FieldLocationRequiredDescriptionExample / default
idpathYesBusiness ID.-
withdrawalIdpathYesWithdrawal ID.-

Cautions

  • Payout API keys receive `404` for withdrawals created by another key or surface.

Common errors

  • 401: Missing or invalid API key.
  • 404: Requested resource does not exist or is not owned by the business.

Sample response

json
{
  "id": "wdr_123",
  "status": "pending",
  "state": "queued",
  "requestedAmount": {
    "raw": "500000000",
    "decimals": 6,
    "display": "500",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "payoutAmount": {
    "raw": "498800000",
    "decimals": 6,
    "display": "498.8",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "withdrawalFee": {
    "raw": "1200000",
    "decimals": 6,
    "display": "1.2",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "destination": "0x5fBDB2315678afecb367f032d93F642f64180aa3"
}

cURL example

bash
curl -X GET "https://api.paychainhq.io/api/v1/businesses/biz_123/withdrawals/wdr_123" \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_your_business_key"

Node.js example

ts
const response = await fetch('https://api.paychainhq.io/api/v1/businesses/biz_123/withdrawals/wdr_123', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'pk_live_your_business_key',
  },
});
const payload = await response.json();
console.log(payload);
PATCH/api/v1/businesses/:id/withdrawals/:withdrawalId/cancelx-api-keySandbox + live

Cancel withdrawal

Cancel a queued withdrawal before it reaches an irreversible stage.

When to use it

Use for operator intervention when a payout should not proceed.

Idempotency: recommended

FieldLocationRequiredDescriptionExample / default
idpathYesBusiness ID.-
withdrawalIdpathYesWithdrawal ID.-

Cautions

  • Once broadcast starts, cancellation may no longer be possible. Inspect the returned status after the call.

Common errors

  • 401: Missing or invalid API key.
  • 404: Requested resource does not exist or is not owned by the business.

Sample response

json
{
  "id": "wdr_123",
  "status": "pending",
  "state": "queued",
  "requestedAmount": {
    "raw": "500000000",
    "decimals": 6,
    "display": "500",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "payoutAmount": {
    "raw": "498800000",
    "decimals": 6,
    "display": "498.8",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "withdrawalFee": {
    "raw": "1200000",
    "decimals": 6,
    "display": "1.2",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "destination": "0x5fBDB2315678afecb367f032d93F642f64180aa3"
}

cURL example

bash
curl -X PATCH "https://api.paychainhq.io/api/v1/businesses/biz_123/withdrawals/wdr_123/cancel" \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_your_business_key" \
  -H "Idempotency-Key: example-request-001"

Node.js example

ts
const response = await fetch('https://api.paychainhq.io/api/v1/businesses/biz_123/withdrawals/wdr_123/cancel', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'pk_live_your_business_key',
    'Idempotency-Key': 'example-request-001',
  },
});
const payload = await response.json();
console.log(payload);
Withdraw funds | PayChainHQ