# Citadel API ## Docs - [Introduction](/index): Issue deposit addresses, send transactions, run workflows, and hear back by webhook. - [Evaluation stack (Docker Compose)](/merchant-stack): Run OpenBao, the policy engine and the signer on one machine with Docker Compose, to try the bring-up order before you plan a production deployment. - [Production architecture](/merchant-stack-production): How the merchant stack runs in production — two host roles, three-node OpenBao clusters with KMS auto-unseal, a signer that accepts no connections, and one command per role, as a Helm install or with the VM installer. - [Monitoring](/merchant-stack-monitoring): What the policy engine, the signer and their OpenBao vaults tell you, how to collect it with an OpenTelemetry Collector, what to put on a dashboard and what to alert on. - [Agent skill: merchant stack setup](/merchant-stack-agent-skill): A skill file that lets a coding agent stand up, check, restart and tear down a merchant stack, step by step, from a new organization to the first signed payout. - [Authentication](/authentication): An API key says who you are. Your own signature says what may move. - [Idempotency](/idempotency): Retry any write without paying twice. - [Lists and pagination](/pagination): Every list names its network, runs newest first, and pages with a cursor. - [Errors](/errors): Two kinds of failure — the request was refused, or the transaction failed later. - [Webhooks](/webhooks): Signed POSTs when something you care about changes. - [Notification channels](/notification-channels): Email lists, Telegram chats, and Slack and Mattermost channels your organization's messages go to. - [Address book](/address-book): Keep your whitelist in step from your backend. Your policy key signs each change, and people approve it before your engine enforces it. - [List wallets](/api-reference/wallets/list-wallets): Your wallets on one network, newest first. `network` is required. Token sub-accounts that Citadel keeps under a wallet (a TON jetton wallet, a Solana token account) are not listed: their money shows on the wallet that owns them. Paged with a cursor — see Lists and pagination. Needs the `read` scope. A key limited to vaults lists the wallets in them. - [Issue a deposit wallet](/api-reference/wallets/issue-a-deposit-wallet): Issues a wallet for one of your users on one chain and network. `network` is required: there is no default network. `end_user_ref` names the user and is required too. Idempotent on `(reference, network)`: the same reference returns the existing wallet with `200`. Issuance is asynchronous — your own signer derives the key — so a fresh wallet answers `202` with `status: creating` and no address. The address arrives with the `wallet.active` webhook or a later `GET /wallets/{reference}`. Needs the `wallets.write` scope. A key limited to vaults issues only into them. - [Issue wallets in a batch](/api-reference/wallets/issue-wallets-in-a-batch): Issues up to 100 wallets in one call — for instance when your users move over from another system. `network` is named once, for the whole batch: every wallet is issued on it. Each item of `wallets` is what `POST /wallets` takes (`reference`, `chain`, `end_user_ref`, and optionally `label`, `tags`, `vault`, `account_implementation`) and is issued or found the same way: a reference that already names a wallet on the network returns that wallet as it is. All or nothing. An item that is refused refuses the whole batch with `422`, its index in the message (`wallets[3]: chain is not supported`), and nothing is created. Fix it and send the same batch again: the wallets it names that already exist come back as they are, the rest are issued. An item may repeat the batch's `network`, but not name another one; a reference may appear once per batch; any other field is refused. `data` lists the wallets in the order you sent them. A new wallet answers `status: creating` without an address, as with `POST /wallets`: each one gets its `wallet.active` webhook when your signer has derived it. Your signer derives them one after another, so the addresses of a large batch arrive over a while. Needs the `wallets.write` scope. A key limited to vaults issues only into them; an item whose `vault` is outside them, or whose reference names a wallet outside them, refuses the batch. - [Get a wallet](/api-reference/wallets/get-a-wallet): Reads a wallet by your reference (within `network`) or by its id. `network` is required; a wallet named by its id must live on it, else `422 network_mismatch`. Needs the `read` scope. A wallet outside the key's vaults answers `404`. - [Update a wallet](/api-reference/wallets/update-a-wallet): Changes what you say about a wallet: its `tags`, its `label`, and the user it belongs to (`end_user_ref`). Send only what changes; a field you leave out keeps its value. `tags` replaces the whole list, and `[]` clears it. Workflows pick wallets by their tags. Nothing else changes. The address, `chain`, `network`, `reference` and `vault` are the wallet itself: a body that sends one of them, or any other field, is refused with `422` and changes nothing. A token sub-account Citadel keeps under a wallet (a TON jetton wallet, a Solana token account) is not changed on its own: it follows its wallet's `end_user_ref`. A new `end_user_ref` applies from now on: lists filter on it, and the network-fee top-ups Citadel sends the wallet carry it. Transactions already made keep the `end_user_ref` they were sent with. Your policy engine counts per-user limits on the `end_user_ref` each transaction is signed with, not on the wallet's. `network` is required: a reference names a wallet within it. Needs the `wallets.write` scope. A wallet outside the key's vaults answers `404`. - [Retire a wallet](/api-reference/wallets/retire-a-wallet): Takes an active wallet out of circulation — a user who left, an address you no longer show. It ends `retired` for good: a retired wallet does not come back, and a new address for the user is a new wallet, under a new reference. The `wallet.retired` webhook follows. A retired wallet is out of circulation, not out of sight: - its address is still watched: a deposit that still arrives on it is credited to it, and reported as any deposit is (`blockchain_event.confirmed`, `GET /transfers`); - it still sends with `POST /transactions`, so you can move out what arrives; - workflows no longer pick it, and a workflow step does not send from it; - `GET /wallets` lists it with `status: retired`, and `GET /wallets/{reference}` reads it. Retiring it again answers the same `200`, without a second webhook. A wallet with no address does not retire: `409 not_retirable` while it is `creating`, and for a `failed` one. A token sub-account Citadel keeps under a wallet follows that wallet: it is `retired` with it, also one Citadel opens later, so no workflow reaches the wallet through its address. Retiring one on its own is refused with `422`. `network` is required: a reference names a wallet within it. Needs the `wallets.write` scope. A wallet outside the key's vaults answers `404`. - [Get a wallet balance](/api-reference/wallets/get-a-wallet-balance): The ledger's balance of one wallet, per asset, in base units. `spendable` is `balance - escrow`: what is on chain minus what transactions in flight have reserved. The wallet's figures include the token sub-accounts Citadel keeps under it (a TON jetton wallet, a Solana token account): a token deposited there counts here, and so does the coin a jetton wallet holds for its fees. An asset Citadel does not list in `GET /assets` — never listed, or disabled since — reads as its contract, with `decimals: null`, as in `GET /balances`. Needs the `read` scope. A wallet outside the key's vaults answers `404`. - [Get a wallet's limits](/api-reference/wallets/get-a-wallets-limits): What your policy engine last said about the limits a payout from this wallet runs into: the wallet's own ceiling (`scope: wallet`) and, when the wallet has an `end_user_ref`, that end user's (`scope: end_user_ref`). For reference only, before a payout: your policy engine decides every payout on its own count, so a payout that fits here can still fail `policy_refused`. The wallet's figure counts every payout from it. The end user's counts the payouts that name them as `end_user_ref`, from any wallet, to addresses outside your whitelist. A payout to a whitelisted address goes without a request signature, so your engine does not take its `end_user_ref` on trust: it counts it under the reserved reference `system:whitelisted`, shared by every payout to your whitelist, and it is not in the end user's figure. Moves between your own wallets count against no limit. Citadel computes no limit. Your engine signs its counters with every payout it approves, and this is the last one it signed for the wallet and for its end user. Each counter is as of its `as_of`: since then, spends older than the window have rolled off, and payouts your engine signed later are not in it until their decision arrives. By `window_end`, everything counted in it has rolled off. Until your engine approves a payout from the wallet or for its end user, there are no counters: `limits` is empty — an unknown ceiling, not a zero. `network` is required. Needs the `read` scope. A wallet outside the key's vaults answers `404`. - [List transactions](/api-reference/transactions/list-transactions): The transactions you sent with `POST /transactions` on one network, newest first, queued ones included. Each row is the body `GET /transactions/{id}` reads. Use it to reconcile your payouts, or to catch up after missed `operation.*` webhooks. Only what your API keys submitted is listed. Transactions Citadel makes for itself (network fee top-ups, sweeps between your wallets, energy) are not, nor are the ones a workflow sends: those are on the workflow's run (`GET /runs/{id}`, the `run.*` webhooks). `network` is required. Paged with a cursor — see Lists and pagination. Needs the `read` scope. A key limited to vaults lists the transactions of wallets in them. - [Send a transaction](/api-reference/transactions/send-a-transaction): Submits one on-chain transaction from a wallet as generic `actions[]`: a native transfer, a token transfer, an approve, a swap, a call to a registered contract. The answer is `202` with the transaction — the body `GET /transactions/{id}` reads; the outcome is asynchronous — poll `GET /transactions/{id}` or take the `operation.*` webhooks. `network` is required: there is no default network. A `wallet` named by reference or address is looked up on it; a wallet named by its id must live on it, else `422 network_mismatch`. The network is checked before anything is reserved. Funds are reserved at the door: the amounts and the network fee stop being spendable before the answer. Your approval rules may hold the transaction for a passkey quorum: still `202`, with `status: awaiting_approval` and an `approval` block. `queue: true` is different: nothing is reserved and nothing is sent. The payout waits for a workflow to send it in a batch (see `queue`). Needs the `transactions.write` scope. From a wallet outside the key's vaults: `403 vault_limited`. `POST /intents` is the same endpoint under its older name. - [Estimate a transaction](/api-reference/transactions/estimate-a-transaction): Takes the body of `POST /transactions` and answers what sending it now would do, without sending anything: the network fee Citadel would hold next to the amounts, and whether your approval rules would hold it for a passkey quorum — how many approvals, from which group, why, and for how long. Nothing is created, reserved or signed, and no webhook follows. It refuses what a send refuses, with the same status and `code`: an unknown wallet, a bad amount or address, a wallet outside the key's vaults (`403 vault_limited`), a wallet id from another network (`422 network_mismatch`). `network` is required. `queue`, `tags` and the idempotency key are not read: an estimate is for sending now. `network_fee` is the most the network may charge the sending wallet, in its native coin — what a send holds for it. Citadel prices it for EVM wallets that pay their own gas. For every other wallet (TRON, TON, Solana, EVM smart accounts) it is `null`: a send holds nothing for the fee, and Citadel does not price it. What it does not promise. Your policy engine decides only when the transaction is signed, so an estimate never asks it. It does not check the balance: the send may still be refused for insufficient funds. And by the time you send, the balance, the fee and the windows of your velocity rules may have moved — the send is judged afresh. Needs the `transactions.write` scope. Your approval rules judge the calling key (its age, what it sent recently), so estimate with the key that will send. - [Get a transaction](/api-reference/transactions/get-a-transaction): Reads a transaction by its `operation_id` or by the `Idempotency-Key` you sent it with — so a client that lost the response can still find it. It takes no `network`: the id is enough, and the answer carries the `network` the transaction is on. The body is everything you need to book it: the wallet it leaves from, its `actions` with each recipient, amount and asset (symbol, contract, decimals), your `end_user_ref` and `tags`, the network `fee` the landed transaction cost, and its times. One that ended without moving money carries `failure`; one declared expired that landed after all carries `corrected`, as the `operation.confirmed` webhook did. A transaction your approval rules held carries its `approval`: the quorum, its state, and who decided when. Needs the `read` scope. A transaction from a wallet outside the key's vaults answers `404`. - [Cancel a transaction](/api-reference/transactions/cancel-a-transaction): Withdraws a transaction while nothing of it is signed yet: - held by your approval rules (`status: awaiting_approval`): its quorum request closes (`approval.status: cancelled`), so nobody can approve it any more, and the amounts and the network fee held for it are spendable again; - queued (`queue: true`) and not yet picked up by a workflow's batch: it held nothing. The transaction ends `failed` with `failure.reason: cancelled`, and the `operation.failed` webhook follows. The answer is the transaction, the body `GET /transactions/{id}` reads. Cancelling it again answers the same `200`. Otherwise it is `409 not_cancellable`, and nothing changes. From `authorized` on, the transaction is on its way to your signer, signed, on chain or ended, and the `error` says which. A quorum that has already decided stands: approved, it is being sent; rejected or expired, it fails on its own. A queued payout a batch already carries goes with the batch. Only what your API keys sent can be cancelled: a transaction a workflow sent stops with its run (`POST /runs/{id}/cancel`), and the ones Citadel makes for itself cannot be. Takes the `operation_id` or the idempotency key, like `GET /transactions/{id}`, and no `network`. Needs the `transactions.write` scope. A transaction from a wallet outside the key's vaults answers `404`. - [Speed up a stuck EVM transaction](/api-reference/transactions/speed-up-a-stuck-evm-transaction): Signs an EVM transaction again under the same nonce with higher fees, so the network takes it in place of the version that waits: one stuck behind its nonce (the `operation.stuck` webhook, `stuck` on the transaction), or one the nodes hold that no block mines because its fee is below what the network asks now. The new version moves exactly the same money from the same wallet to the same recipient; only the fees change, each at least 12.5% above the waiting version's, or the network's current price when that is higher. Both versions share one nonce, so at most one of them is ever mined and the payout cannot be paid twice. Your policy engine signs the new version only on these terms, and does not count it against your limits again. The extra network fee the higher cap may cost is held in the wallet's native coin until the transaction settles. The answer is `202` with the transaction. Its `speedup` reads `requested` until your signer answers, then: - `signed`: the new version is sent in place of the old one, and `tx_hash` is its hash. If the network mines the old version after all, the transaction confirms under the old hash: `tx_hash` and the `operation.confirmed` webhook name the version that landed. - `refused`: the new version is not sent, and `speedup.reason` says why: your policy engine or signer refused it, every node rejected it, or no answer came within its 10-minute signing window. The waiting version, its status and its holds stay as they were. You may ask again. It is `409 not_speedable`, and nothing changes, when: - the transaction is not an EVM transfer from a standard account, or it is sponsored. TRON, Solana and TON transactions never wait behind a nonce: they expire with their validity window, and their funds return; - it is not signed yet, or it has ended; - its current version has waited less than 5 minutes; - a speedup of it is still waiting for your signer; - its nonce is already mined, so it settles on its own; - the wallet cannot cover the higher network fee. A higher fee does not help a transaction stuck because the wallet lacks the coin for gas (`insufficient funds` in `stuck.error`): top the wallet up instead, and the waiting version goes through as it is. Takes the `operation_id` or the idempotency key, like `GET /transactions/{id}`, and no `network`. Needs the `transactions.write` scope. A transaction from a wallet outside the key's vaults answers `404`. - [Validate an address](/api-reference/transactions/validate-an-address): Whether an address can be the destination (`to`) of a transaction on a chain: the same check `POST /transactions` runs, so your UI can refuse a mistyped withdrawal address before a payout is sent. A TRON address is checked down to its checksum, a TON user-friendly one to its CRC, an EVM one to `0x` and 40 hex digits, a Solana one to a 32-byte key. An invalid address answers `200` with `valid: false` and the `reason`. It says nothing about who holds the address, or whether your policy engine will allow paying it. `network` is required. Needs the `read` scope. - [List transfers](/api-reference/transfers/list-transfers): What moved on chain through your wallets on one network, newest first: deposits (`direction: in`), payouts (`out`), and moves between your own wallets (`internal`). Each row is the body of the `blockchain_event.confirmed` webhook, so you credit a deposit the same way from either, plus `aml`: the compliance screenings your workflows ran for it since. Use it to catch up on deposits whose webhook you missed. Only confirmed transactions are listed: Citadel reports one once a reorganization can no longer undo it. A transaction that moved no value (an approve, a plain contract call) is not a transfer; read it with `GET /transactions/{id}`. `network` is required. Paged with a cursor — see Lists and pagination. Needs the `read` scope. A key limited to vaults lists the transfers that touch a wallet in them, and a leg of a wallet outside them reads `wallet: null`. - [Get a transfer](/api-reference/transfers/get-a-transfer): One transfer by its transaction hash, on `network` — the answer to "I sent it, where is it?". `404` until it is confirmed: Citadel reports a transaction once it is final. The body is a row of the list: the `blockchain_event.confirmed` webhook's, plus `aml`. Read it again after a screening workflow ran to see the provider's reply. Needs the `read` scope. A transfer that touches no wallet in the key's vaults answers `404`. - [List networks and chains](/api-reference/reference/list-networks-and-chains): The networks your installation serves and, on each, the chains it indexes — the values `network` and `chain` take everywhere else. EVM has one instance per L1/L2, and a transfer names the `instance` it landed on. `enabled` is your organization's own switch for a chain: off refuses new wallets there, while wallets already issued keep being indexed. Takes no `network`. Needs the `read` scope. - [List assets](/api-reference/reference/list-assets): What Citadel credits on one network, per chain instance: the symbol, the contract that identifies a token (`null` for the native coin), and the `decimals` every base-unit amount of that asset is scaled by. A token not listed is not credited. `network` is required. Needs the `read` scope. - [List asset prices](/api-reference/reference/list-asset-prices): A price in US dollars for every asset `GET /assets` lists on one network — to show an amount in dollars next to its base units. For display only: Citadel locks no rate, and nothing is charged or settled at this price. `usd` is the median of the recent quotes of the asset's market from the price sources your installation asks, as a decimal string; `as_of` is the time of the oldest quote it is made of. Quotes older than the installation's freshness limit (10 minutes unless configured otherwise) are not used: an asset with no recent quote has `usd: null`, never a guess. Your approval rules judge their dollar steps by the highest recent quote, not this median, so the two can differ slightly. A test network's tokens are priced as the market they stand for. `network` is required. Needs the `read` scope. - [Get network status](/api-reference/reference/get-network-status): How each chain of one network is doing now: whether Citadel's index keeps up with the chain, and whether Citadel has a chain node to build and send transactions through. Look here when deposits arrive late or sends fail with `rpc_unavailable`, to tell a chain or index delay from a problem on your side. `indexing.state`: | `state` | | |---|---| | `live` | The index keeps up with the chain. | | `catching_up` | Behind, past a quarter of the chain's lag threshold; deposits arrive later than usual. | | `stalled` | Far behind the chain head, or not advancing. | | `backfilling` | Indexing a stretch of blocks it missed; deposits from it arrive as it goes. | | `unreachable` | The chain cannot be reached. | | `stale` | No recent word from the indexer; the figures are as of `as_of`. | `head` is the chain's latest block as Citadel's nodes see it, `indexed` the last block Citadel has indexed, `lag_blocks` the difference. `since` is when the current trouble began (`null` when there is none). `nodes` is `available` with a node in rotation, `degraded` when only slow or lagging nodes are left, `unavailable` when there is none: a transaction cannot be built or sent on that chain until one is back. `network` is required. Needs the `read` scope. - [List vaults](/api-reference/organization/list-vaults): Your organization's vaults the key reaches, oldest first — the ids `vault` takes in `POST /wallets`, `GET /wallets` and `GET /balances`. The first is where `POST /wallets` issues when the request names no `vault`. Takes no `network`: a vault holds wallets on every network. Needs the `read` scope. A key limited to vaults lists those vaults only. - [Get organization balances](/api-reference/organization/get-organization-balances): What your organization holds on one network, in base units, per chain instance and asset: the totals of `GET /wallets/{reference}/balance` over every wallet, without a request per wallet. With `vault`, over that vault only. `balance` is what the chains hold, `escrow` what transactions in flight have reserved out of it, `staked` what is locked, and `spendable` is `balance - escrow`. The same token on two chain instances is two rows, never one sum: USDT on TRON and on BSC are two contracts, with their own `decimals`. An asset Citadel does not list in `GET /assets` reads as its contract, with `decimals: null`. `network` is required: each network keeps its own books, and a total never spans them. Needs the `read` scope. A key limited to vaults totals those vaults only; a `vault` outside them answers `404`. - [Get the calling key](/api-reference/organization/get-the-calling-key): What the key you call with is: the organization it acts for, what it was minted with (scopes, vaults, IP allowlist, expiry), the networks your installation serves, and the address your call arrived from — the one the IP allowlist judges. Use it to check an integration end to end. The token itself is never returned: Citadel keeps only its digest. Takes no `network`. Needs the `read` scope, like every `GET`. - [Get the billing balance](/api-reference/organization/get-the-billing-balance): Your organization's prepaid balance for paid workflow steps — TRON energy and bandwidth, account activation, compliance screening — in micro-USDT (`decimals: 6`), as integer strings. A paid step holds its estimated price before it asks the provider and settles to the actual price after; when `spendable` cannot cover the hold, the step does not run and the workflow follows its "unavailable" rule. `available` is the balance; `held` what running steps hold; `receivable` what a provider charged beyond what the balance could cover, which your next top-up pays first; `credit_limit` how far below zero `available` may go (`0` — prepaid); `spendable` what a new hold may still take, `available + credit_limit`. `open_holds` counts the steps still holding. One balance for the organization: it takes no `network`. Needs the `read` scope. - [List approvals](/api-reference/approvals/list-approvals): What waits, or waited, for people on one network, newest first: transactions your approval rules held for a passkey quorum (`kind: transaction`), and workflow runs stopped at a manual approval step (`kind: run`). With `status=pending`, what waits now and until when (`expires_at`). Approvals of changes made in the console (approval rules, workflows) are not listed. Read only. People approve or reject in the console, each with their own passkey; an API key can neither approve nor reject. A pending approval past `expires_at` reads, and is filtered as, `expired`. A transaction's row carries the `approval` block that `GET /transactions/{id}` shows; a run's row names its workflow and the step's title and message. `network` is required. Paged with a cursor — see Lists and pagination. Needs the `read` scope. A key limited to vaults lists the transactions from wallets in them only, and no runs: a workflow chooses its own wallets. - [Get the policy document](/api-reference/policy/get-the-policy-document): The policy document last published to your policy engine: the whitelist, limits and networks it enforces. `document` is the exact YAML your policy key signed, byte for byte, and `signature` your key's Ed25519 signature over those bytes (base64): check it with your own key's public half, and compare the document with the one you signed. Your engine verifies the signature before it installs a document; what it enforces is its own copy. One document rules every network and every vault: it takes no `network`. Needs the `read` scope. A key limited to vaults is refused with `403 vault_limited`. Before the first document is published, `404`. - [Draft the policy document](/api-reference/policy/draft-the-policy-document): Drafts a new version of your policy document from your backend: the whitelist of each chain (your address book), the assets your engine recognises, the ceilings and limits. See [Address book](/address-book) for the whole round. The body is the **whole** document — what you leave out is not in it. An empty whitelist lets your engine pay no external address; an asset without a ceiling cannot be sent. Citadel stamps the next `version` and each chain's module version and answers `document`: the exact YAML bytes your policy key signs. Before you sign, parse `document` and check that it says what you sent. Then sign those bytes, unchanged, with your policy key (key B) and hand the signature to `POST /policy/publish`. There is one draft per organization: a new draft replaces the one waiting to be signed, whether it was made here or in the console. Rules equal to the change already waiting for approval answer that change (`200`, `status: pending`) and leave the draft as it was. Rules equal to the version in force are refused with `422 nothing_changed`. A field the document does not have, an address its chain cannot parse, an amount that is not an integer string, or a window that is not a duration is refused with `422` before the draft is replaced. One document rules every network and every vault: it takes no `network`. Needs the `policy.write` scope. A key limited to vaults is refused with `403 vault_limited`. - [Get a policy draft](/api-reference/policy/get-a-policy-draft): One version of the policy document, drafted here or in the console, and what became of it: - `draft` — sign `document` and publish it; - `pending` — signed; waits for approval (`approval`); - `published` — sent to your engine; `GET /policy` reads what is in force; - `rejected` — never sent; `rejected_reason` says why. Read it to follow a change after `POST /policy/publish`, or to fetch a draft's bytes again before you sign. No `network`. Needs the `read` scope. A key limited to vaults is refused with `403 vault_limited`. - [Publish a signed policy draft](/api-reference/policy/publish-a-signed-policy-draft): Hands in your policy key's signature over a draft's bytes. Citadel never signs a policy document: your engine installs only what your policy key (key B) signed. Citadel checks the signature against the policy key your engine registered when it joined. The draft does not go to your engine yet. It becomes `pending`, and an approval request opens in the console with the bar of your approval rules in force: as many approvals as their strictest rule asks, from that rule's group. When no approval rules are in force, or they hold nothing, one member of your organization approves. Approvers confirm with their passkeys and see every whitelist address added and removed, in full. Once approved, Citadel sends the document to your engine (`published`), which verifies the signature again and installs it. Declined or not approved in time, it is `rejected` and never sent. A newer change replaces one still waiting, and so does a newer version published in the console: the older one reads `rejected`, `rejected_reason: superseded`. A rejected version number is never used again. Sending the same `version` and `signature` again answers the change as it stands, so a retry after a lost answer is safe. No `network`. Needs the `policy.write` scope. A key limited to vaults is refused with `403 vault_limited`. - [Start workflows by event](/api-reference/workflows/start-workflows-by-event): Sends your own named event (`payout`, `refund`, …) on one network with the inputs your workflow declared. Every active workflow of that network listening on the name starts a run; a workflow on another network never hears it. The platform does the rest — screening, choosing a wallet, moving funds — the way the workflow is drawn. `network` is required: there is no default network. Inputs are type-checked at the door on that network: a wallet must be yours there, an asset supported there. Idempotent per (event, idempotency key, workflow): a retry with the same inputs returns the runs it started and starts nothing twice. The key reused with other inputs, or on another network, is refused with `409 idempotency_conflict`, and nothing starts. Needs the `events.write` scope. A key limited to vaults is refused with `403 vault_limited`: workflows choose their own wallets. - [List workflow runs](/api-reference/workflows/list-workflow-runs): The workflow runs on one network, newest first. Each row is the body `GET /runs/{id}` reads. Use it to reconcile your `POST /events` calls, or to catch up after missed `run.*` webhooks. Every run a workflow started is listed: by your event, and by Citadel itself (a deposit, a schedule). A run's `event` and `idempotency_key` are those of the `POST /events/{name}` call that started it, and `null` for a run Citadel started. The steps inside a run (a loop's iterations, a workflow it calls) are part of that run, not rows of their own. `network` is required. Paged with a cursor — see Lists and pagination. Needs the `read` scope. A key limited to vaults is refused with `403 vault_limited`. - [Get a workflow run](/api-reference/workflows/get-a-workflow-run): Where a run is, what started it, what it produced, why it stopped. It takes no `network`: the id is enough, and the answer carries the `network` the run's workflow is on. Needs the `read` scope. A key limited to vaults is refused with `403 vault_limited`. - [Cancel a workflow run](/api-reference/workflows/cancel-a-workflow-run): Stops a run you started by mistake, while nothing it asked for can be signed yet. The run ends `cancelled`, with `failure_reason: cancelled through the API`, and is unwound the way a failed run is: what its steps held goes back — a wallet it locked, a provider hold nobody used — and what it waits for no longer wakes it. A pending approval it asked for closes, so nobody can approve it any more. The loop iterations and the workflows it called stop with it. The `run.cancelled` webhook follows. Cancelling it again answers the same `200`. Otherwise it is `409 not_cancellable`, and nothing changes: - a transaction the run authored has reached `authorized`: it is on its way to your signer, and a signature cannot be called back. Follow the run to its end instead; - the run has ended (`completed`, `failed`); - it is executing a step this moment. That lasts a moment: send the same call again; - it runs inside another run (a loop iteration, a workflow another one called): cancel the run that started it. A paid step (an energy rental, a screening) already handed to its provider is bought work: it is billed whether or not the run goes on. It takes no `network`: the id is enough. Needs the `events.write` scope — the scope that starts runs. A key limited to vaults is refused with `403 vault_limited`. - [List workflows](/api-reference/workflows/list-workflows): The active, published workflows on one network, by name. For a workflow your backend starts, `event` is the name `POST /events/{name}` takes and `inputs` what it declares: each input's key, type and whether it is required. A workflow Citadel starts itself (on a deposit, on a schedule) or another workflow calls has `event: null` and no inputs. `name` is what `GET /runs` filters on. An input's `type` is what `POST /events/{name}` checks its value against: `wallet` one of your active wallets on the network (reference, id or address), `asset` an asset credited there (symbol or contract), `address` a TRON address, `amount` a base-unit integer, `number` an integer, `decimal` a decimal number, `boolean` `true` or `false`, `string` anything. `network` is required. Needs the `read` scope. A key limited to vaults is refused with `403 vault_limited`, like the event door. - [List webhook endpoints](/api-reference/webhook-endpoints/list-webhook-endpoints): Your organization's webhook endpoints, oldest first, in one response. A row never carries the signing secret: it is shown once, when the endpoint is added or its secret rotated. Takes no `network`: an endpoint hears every network. Needs the `read` scope. A key limited to vaults is refused with `403 vault_limited`: an endpoint serves every vault. - [Add a webhook endpoint](/api-reference/webhook-endpoints/add-a-webhook-endpoint): Registers a URL to receive webhooks. Citadel generates its signing secret and shows it in this answer only: store it now. Lost it? Rotate the secret. One endpoint per URL: a URL your organization already has answers `409 endpoint_exists` and adds nothing, so an add sent twice (a lost response, a retried script) never registers the URL twice. Change the existing one with `PATCH` instead. `url` is an `https://` URL with a host; `https://` alone, or a URL with a space in its host, answers `422`. A field the endpoint does not take (`secret`, `network`, …), or a field sent in the query string instead of the body, is refused with `422`, never ignored. Needs the `webhooks.write` scope. A key limited to vaults is refused with `403 vault_limited`. - [Get a webhook endpoint](/api-reference/webhook-endpoints/get-a-webhook-endpoint): One endpoint, without its signing secret. Needs the `read` scope. A key limited to vaults is refused with `403 vault_limited`. - [Remove a webhook endpoint](/api-reference/webhook-endpoints/remove-a-webhook-endpoint): Removes the endpoint for good, with its deliveries: pending ones are not sent, and its rows leave `GET /webhook_deliveries`. To stop deliveries for a while, pause it instead. Needs the `webhooks.write` scope. A key limited to vaults is refused with `403 vault_limited`. - [Change a webhook endpoint](/api-reference/webhook-endpoints/change-a-webhook-endpoint): Changes what the body names, nothing else: `url`, `description`, `event_types`, `state`. All of it or nothing — a change that is refused leaves the endpoint as it was. `state: paused` stops deliveries until you resume. `state: active` resumes an endpoint you paused, and one Citadel disabled after 5 deliveries in a row exhausted their attempts; it clears that count. Failed deliveries stay failed: send them again with `POST /webhook_deliveries/retry`. A `url` another endpoint of yours has answers `409 endpoint_exists`; one that is not an `https://` URL with a host answers `422`. A field the endpoint does not take (`secret`, `network`, …), a field sent in the query string instead of the body, or an empty body, answers `422`. Needs the `webhooks.write` scope. A key limited to vaults is refused with `403 vault_limited`. - [Send a test delivery](/api-reference/webhook-endpoints/send-a-test-delivery): Posts a `test.ping` to the endpoint now: a real delivery, signed and retried like any other, with an empty `data`. The answer is that delivery; follow it in `GET /webhook_deliveries` (`?endpoint=&type=test.ping`). An endpoint that is not active (paused, or disabled by Citadel) answers `409 endpoint_disabled`: resume it first. Needs the `webhooks.write` scope. A key limited to vaults is refused with `403 vault_limited`. - [Rotate the signing secret](/api-reference/webhook-endpoints/rotate-the-signing-secret): Generates a new signing secret and shows it in this answer only. The secret it replaces keeps signing next to it for `overlap_seconds`: until `previous_secret_expires_at`, every delivery carries two `v1` signatures in `X-Citadel-Signature`, the new secret's first. Deploy the new secret to your verifier within that time and no delivery is rejected on the way. Only two secrets sign at once. Rotating again during an overlap retires the older one at once; the secret being replaced becomes the previous one. `overlap_seconds: 0` retires the old secret now — deliveries already on their way may then fail your check once and are retried with the new one. Not idempotent: every call makes a new secret. A retry after a lost answer replaces the secret of that answer, so the one your verifier holds stops signing at once: deploy the secret from the last answer you got. `overlap_seconds` goes in the body; in the query string it answers `422`. Needs the `webhooks.write` scope. A key limited to vaults is refused with `403 vault_limited`. - [List webhook deliveries](/api-reference/webhook-deliveries/list-webhook-deliveries): What Citadel posted, or still tries to post, to your webhook endpoints, newest first: one row per event and endpoint, with its attempts and the endpoint's last answer. `id`, `type`, `resource` and `data` are the body the endpoint receives, without the per-attempt `timestamp`, so a row you missed can go through your webhook handler, deduplicated on `id`. Takes no `network`: an endpoint hears every network. A paused or disabled endpoint gets no deliveries: an event that happened meanwhile has no row here — read it from `GET /transfers` or `GET /transactions`. Paged with a cursor — see Lists and pagination. Needs the `read` scope. A key limited to vaults is refused with `403 vault_limited`: an endpoint serves every vault. - [Retry a failed delivery](/api-reference/webhook-deliveries/retry-a-failed-delivery): Sends one failed delivery again. It goes back in line with the whole retry schedule ahead of it, first attempt now, and the answer is the delivery as it now stands: `pending`, no attempts yet. Same `id`, same payload: your endpoint deduplicates it like any retry. Only a `failed` delivery: a pending one is already on its way, a delivered one arrived — both answer `409 delivery_not_failed`. A delivery whose endpoint is paused or disabled answers `409 endpoint_disabled`: resume the endpoint first (`PATCH /webhook_endpoints/{id}` with `state: active`). Needs the `webhooks.write` scope. A key limited to vaults is refused with `403 vault_limited`. - [Retry the failed deliveries of a period](/api-reference/webhook-deliveries/retry-the-failed-deliveries-of-a-period): Sends every failed delivery recorded in a period again — how you catch up once your endpoint is back. Each goes back in line as with a single retry, keeping its `id`. Oldest first, at most 500 per call: while `more` is `true`, send the same call again. Calling again is safe, because what one call sent is pending by the next, not failed. A failed delivery whose endpoint is paused or disabled stays failed and counts in `skipped`: resume the endpoint (`PATCH /webhook_endpoints/{id}` with `state: active`), then call again. Takes the list's filters except `status`. Needs the `webhooks.write` scope. A key limited to vaults is refused with `403 vault_limited`. - [Ask for a report](/api-reference/reports/ask-for-a-report): Asks for a report as a file, PDF or CSV, on one network. Citadel builds it in the background: the answer is `202` with the report `queued`. Follow it with `GET /reports/{id}` until it is `ready`, then fetch the file with `GET /reports/{id}/file`. A report shows the books as they stand when it is built. | `type` | What it holds | `wallet` | `window_days` | |---|---|---|---| | `wallet_statement` | One wallet: its compliance screening, its balances, and every confirmed movement of the window, network fees included, with a running balance per asset | required | optional, 1 to 30, default 30 | | `custody` | The organization: its assets and their estimated USD value, its top counterparties | none | none | | `overview` | The organization: holdings, value per chain, inflow, outflow and fees of the last 30 days, top counterparties, concentration | none | none | Only confirmed transfers are in a report. USD figures are reference estimates, marked as such in the file; no rate is locked with them. A statement has a row for every change to the wallet's balance in the window: each transfer, and a row of its own for what a transaction moved beyond its transfers. The CSV names each row in its `kind` column: | `kind` | The row | |---|---| | `transfer` | A confirmed transfer to or from the wallet | | `fee` | The network fee of a transaction the wallet paid for, in the chain's coin: one it sent, or one it sponsored. Only the payer's statement has it, and all of it: a sponsored transaction's fee is on the sponsor's statement, none of it on the statement of the wallet it ran for | | `stake` | Coin the wallet staked (negative) or unstaked (positive) | | `adjustment` | Any other change the books recorded for the transaction, such as part of a transfer coming back, gas a sponsor advanced and got back, or what receiving cost a TON wallet in a transaction another wallet paid for | The balance before the first row is what the wallet held when the window opened; the balance after the last row is what it holds now. Citadel books a transaction as one change to the balance: when a transaction has several rows, the running balance counts what came in before what went out, so every row of it reads at least the lower of the balance before the transaction and the balance after it, and never below zero. The balance after the transaction is the one on its last row. A statement names its wallet like `GET /wallets/{reference}`: your reference on the network the request names, or the wallet's id. A wallet without an address yet (`creating` or `failed`) and a token sub-account Citadel keeps under a wallet are refused with `422`; ask for the statement of the wallet that owns it. A retired wallet has a statement. A refused request leaves no report behind. Sending the same request again asks for another report. `network` is required. Needs the `read` scope: a report moves nothing and holds only what the reads return. A key limited to vaults is refused with `403 vault_limited`: the organization-wide reports cover every vault, and a statement names the other wallets of the organization it dealt with. - [Get a report](/api-reference/reports/get-a-report): Where a report is: `queued`, `running`, then `ready` with its `file`, or `failed`. A failed report stays failed: ask for a new one. Poll every few seconds until it ends. Every report of your organization reads here, including the ones people asked for in the console and the ones a workflow's report step asked for: that step puts the report's `id` in the run's outputs. It takes no `network`: the id is enough, and the answer carries the `network` the report reads. Needs the `read` scope. A key limited to vaults is refused with `403 vault_limited`. - [Download a report's file](/api-reference/reports/download-a-reports-file): The file of a `ready` report, as an attachment: `application/pdf` or `text/csv`, under the filename `GET /reports/{id}` names. Fetch it as many times as you need. Before the report is ready, `409 report_not_ready`: follow it with `GET /reports/{id}`. A failed report has no file: `409 report_failed`. It takes no `network`. Needs the `read` scope. A key limited to vaults is refused with `403 vault_limited`. - [Operation*](/api-reference/operation*): A transaction you submitted changed state. Types: `operation.confirmed`, `operation.failed`, `operation.expired`, `operation.stuck`, `operation.awaiting_approval`, `operation.approved`. - [Blockchain event*](/api-reference/blockchain_event*): A transaction touching your wallets is final on chain — a deposit is one with `direction: in`. Type: `blockchain_event.confirmed`. Citadel reports a transaction once, when a reorganization can no longer undo it; there is no notice while it is pending. - [Run*](/api-reference/run*): A workflow run ended. Types `run.completed`, `run.failed`, `run.cancelled`. - [Wallet*](/api-reference/wallet*): Issuance settled, or you retired the wallet. `wallet.active` carries the address; `wallet.failed` carries the reason; `wallet.retired` follows `POST /wallets/{reference}/retire`.