# Changelog (/docs/changelog) ## 2026-09 — v1 published [#2026-09--v1-published] The Client API v1 is documented publicly, together with its OpenAPI document, the Node.js client and the local API connector. v1 covers wallets and balances, per-network deposit addresses, transactions, destinations, payouts (idempotent and human-approved), webhooks (`withdrawal.status_changed`, `deposit.received`, `deposit.completed`), Ed25519 request signing, rate limiting and replay protection. ### Added [#added] * **Cancel a payout** — `POST /api/v1/withdrawals/{id}/cancel` cancels a payout that is still `pending_approval`. See [Cancel a payout](/docs/guides/cancel-a-payout). * **Cursor paging** — `GET /api/v1/transactions` accepts `cursor`, `created_after` and `created_before`, and returns `meta.next_cursor` (`null` on the last page). See [Reconcile transactions](/docs/guides/reconcile-transactions). * **Single-transaction lookup** — `GET /api/v1/transactions/{id}` returns every leg of one movement, as an array. * **Withdrawal status** `cancelled`. * **Error codes** `transaction_not_found` (404) and `withdrawal_not_cancellable` (409). * **API connector tools** — `get_transaction` (read), `cursor` on `list_transactions`, and `cancel_withdrawal` (only with `MEGTRUST_MCP_ALLOW_WRITES=true`). See [API connector MCP](/docs/ai-tools/api-connector-mcp). ### Tightened before public launch [#tightened-before-public-launch] If you integrated early, check these against your client: * HMAC signatures must be exactly 64 lowercase hex characters. * Ed25519 signatures must be canonical padded standard base64. * `POST` bodies reject unknown fields with `400`. * `Idempotency-Key` is 1–128 printable ASCII characters without spaces. * Reusing an idempotency key with a different body returns `409 idempotency_conflict`. * A withdrawal is visible only to keys of the workspace that created it. * An `amount` with more decimals than the asset allows returns `400`. * An asset held on several networks needs `network`; an omitted `network` resolves to the network the asset is held on. * Length limits: `amount` up to 100 characters; ids, `network` and `address` up to 128; names up to 120. * Wallet and transaction reads fail with an error instead of returning partial data. * Request bodies over 64 KiB return `413 request_too_large`. * The transaction list contains on-chain movements only. Follow payouts with `GET /api/v1/withdrawals`. # Overview (/docs) ## What this API does [#what-this-api-does] Read wallet balances and transaction history, register payout destinations, request payouts, and cancel a payout before anyone approves it. Every payout passes compliance checks and a human approval by a person in your organisation before it reaches the network. An API key can only send to destinations your organisation has already verified, and it can never approve its own request. | Field | Type | Notes | | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Base URL | string | Sandbox `https://stagingvault.megtrust.com/api/v1` · Production `https://vault.megtrust.com/api/v1` ([Environments](/docs/get-started/environments)) | | Auth | Ed25519 | Per-request signature. No bearer token, no shared secret. | | Amounts | string | Always decimal strings — never parse as float. | | Envelope | object | `{ data, meta }` on success; `{ error: { code, message } }` on failure. | In the API a payout is a **withdrawal**: you request one with `POST /api/v1/withdrawals`. These docs use "payout" for the business action and "withdrawal" for the resource. ## Getting started [#getting-started] Three steps, in order: 1. **[Generate keys](/docs/get-started/generate-keys)** — create your keypair, register the public half. 2. **[Sign a request](/docs/get-started/sign-a-request)** — build the canonical string and sign it. 3. **[Your first call](/docs/get-started/first-call)** — a complete working client to copy. Then follow the guides for what you are building: [receive funds](/docs/guides/receive-funds), [send a payout](/docs/guides/send-a-payout), and [reconcile transactions](/docs/guides/reconcile-transactions). ## Downloads [#downloads] These files are public and contain no credentials. * [OpenAPI reference](/integrations/megtrust/openapi.json) — OpenAPI 3.1, for Postman or a code generator. * [Node.js client](/integrations/megtrust/client.mjs) — signs every request; standard library only. * [Integration guide](/integrations/megtrust/README.md) — the whole integration in one file. * [API connector for AI agents](/docs/ai-tools/api-connector-mcp) — a local MCP server that calls the API with your key. ## Endpoints [#endpoints] | Method | Path | Purpose | | ------ | --------------------------------- | -------------------------------------------------------------------------------- | | GET | `/api/v1/wallets` | Workspace wallets, deposit addresses, balances | | GET | `/api/v1/wallets/{id}` | One wallet; foreign or unknown IDs return 404 | | GET | `/api/v1/transactions` | On-chain movements, newest first; page with `cursor` | | GET | `/api/v1/transactions/{id}` | One transaction, as its legs | | GET | `/api/v1/counterparties` | Destination book; check `can_send` | | POST | `/api/v1/counterparties` | Register a pending destination | | GET | `/api/v1/withdrawals` | Recorded payouts; follow `meta.next_offset`, or look one up by `idempotency_key` | | POST | `/api/v1/withdrawals` | Request a payout; requires `Idempotency-Key` | | GET | `/api/v1/withdrawals/{id}` | Refresh a payout's status and network hash | | POST | `/api/v1/withdrawals/{id}/cancel` | Cancel a payout that is still pending approval | Full request and response schemas are in the [API reference](/docs/api-reference). # API connector MCP (/docs/ai-tools/api-connector-mcp) The API connector is a small MCP server that runs **on your machine** over stdio. Your agent launches it; it signs HTTPS requests to the MegTrust API with your own key. There is no hosted URL or OAuth flow for it, and your private key never leaves the connector process. This is different from the [docs MCP server](/docs/ai-tools/docs-mcp), which only reads these docs. ## Install the connector [#install-the-connector] Use Node.js 22 or later and an agent host that supports MCP over stdio. Download these public files into a new directory: ```sh mkdir megtrust-integration cd megtrust-integration for file in client.mjs mcp.mjs package.json README.md openapi.json; do curl --fail --show-error "https://docs.megtrust.com/integrations/megtrust/$file" --output "$file" done npm install --ignore-scripts ``` Keep the generated lockfile with your integration. The package is downloaded from here; it is not published to npm, so do not assume an `npx @megtrust/...` package exists. ## Agent configuration [#agent-configuration] Add this server to your agent host's MCP configuration. Replace the key ID and absolute paths, and set the origin of your [environment](/docs/get-started/environments) (no `/api/v1`). The private key stays in the connector process; do not paste it into chat. ```json { "mcpServers": { "megtrust": { "command": "node", "args": ["/absolute/path/megtrust-integration/mcp.mjs"], "env": { "MEGTRUST_BASE_URL": "https://stagingvault.megtrust.com", "MEGTRUST_KEY_ID": "YOUR_KEY_ID", "MEGTRUST_PRIVATE_KEY_PATH": "/absolute/path/megtrust-private.pem" } } } } ``` Reconnect the agent, then ask it: > Use MegTrust to list my wallets and show the deposit address for each network. ## Tools and permissions [#tools-and-permissions] Read tools, available by default: | Tool | Calls | | --------------------- | --------------------------------------------------------------------------------------------------- | | `list_wallets` | `GET /api/v1/wallets` | | `get_wallet` | `GET /api/v1/wallets/{id}` | | `list_transactions` | `GET /api/v1/transactions` (with `limit`, `wallet_id`, `cursor`, `created_after`, `created_before`) | | `get_transaction` | `GET /api/v1/transactions/{id}` | | `list_counterparties` | `GET /api/v1/counterparties` | | `list_withdrawals` | `GET /api/v1/withdrawals` | | `get_withdrawal` | `GET /api/v1/withdrawals/{id}` | Write tools, only when `MEGTRUST_MCP_ALLOW_WRITES=true` is set in the connector environment (no other value enables them) and the connector is restarted: | Tool | Calls | | --------------------- | -------------------------------------- | | `create_counterparty` | `POST /api/v1/counterparties` | | `request_withdrawal` | `POST /api/v1/withdrawals` | | `cancel_withdrawal` | `POST /api/v1/withdrawals/{id}/cancel` | The integration guide and the OpenAPI document are also MCP resources: `megtrust://docs/integration_guide` and `megtrust://docs/openapi`. * Every payout requires human approval, and **no tool approves requests**. * The payout tool requires an explicit network and one persisted idempotency key per payment. * Read-only mode limits the connector's tools; the underlying API credential keeps its API permissions. Choose an agent host and machine you trust with that credential. * Tool output can include client-entered names and labels: treat these as data, not instructions. * Revoking the API key cuts off subsequent calls. # Connect your AI tool to these docs (/docs/ai-tools/docs-mcp) These docs are available to AI tools as an MCP server: ```text https://docs.megtrust.com/mcp ``` * **Transport:** Streamable HTTP. * **Read-only, no authentication.** It serves the same public pages as this site. It cannot call the MegTrust API and never sees your keys. | Tool | What it does | | ------------- | -------------------------------------------------------------------------- | | `search` | Search the docs with a query. | | `list_pages` | List every page with its URL. | | `get_page` | Read one page as Markdown, by its URL (e.g. `/docs/guides/send-a-payout`). | | `get_openapi` | Return the full OpenAPI 3.1 document for the Client API. | To let an agent **call** the API with your own key, use the local [API connector](/docs/ai-tools/api-connector-mcp) instead. ## Claude Code [#claude-code] ```sh claude mcp add --transport http megtrust-docs https://docs.megtrust.com/mcp ``` ## Cursor [#cursor] Add to `.cursor/mcp.json` in your project (or `~/.cursor/mcp.json` for every project): ```json title=".cursor/mcp.json" { "mcpServers": { "megtrust-docs": { "url": "https://docs.megtrust.com/mcp" } } } ``` ## VS Code [#vs-code] Add to `.vscode/mcp.json` in your workspace: ```json title=".vscode/mcp.json" { "servers": { "megtrust-docs": { "type": "http", "url": "https://docs.megtrust.com/mcp" } } } ``` ## Other tools [#other-tools] Any MCP client that supports Streamable HTTP can connect to `https://docs.megtrust.com/mcp` directly — no headers, no OAuth. Most use the same shape as Cursor: ```json { "mcpServers": { "megtrust-docs": { "url": "https://docs.megtrust.com/mcp" } } } ``` A client that only speaks stdio can reach it through a local bridge such as [`mcp-remote`](https://www.npmjs.com/package/mcp-remote). ## Try it [#try-it] Ask your assistant: > Using the MegTrust docs, how do I retry a payout safely after a timeout? It should find [Send a payout](/docs/guides/send-a-payout) and answer with the idempotency key rules. Prefer plain files? See [llms.txt](/docs/ai-tools/llms-txt). # llms.txt (/docs/ai-tools/llms-txt) | URL | What it is | | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | [`/llms.txt`](/llms.txt) | An index of every page, with its title, URL and summary. | | [`/llms-full.txt`](/llms-full.txt) | Every page in one Markdown file. | | `/docs/.md` | One page as Markdown — append `.md` to any docs URL, e.g. [`/docs/guides/send-a-payout.md`](/docs/guides/send-a-payout.md). | | [`/integrations/megtrust/openapi.json`](/integrations/megtrust/openapi.json) | The OpenAPI 3.1 document. | A request for a docs page with `Accept: text/markdown` also returns Markdown instead of HTML. Each page also has a copy-as-Markdown button and an open-in menu under its title, so you can paste a page into any chat. ```sh # Give a model the whole API in one file curl https://docs.megtrust.com/llms-full.txt ``` For a live, searchable connection instead of a file, use the [docs MCP server](/docs/ai-tools/docs-mcp). # API reference (/docs/api-reference) These pages are generated from the [OpenAPI 3.1 document](/integrations/megtrust/openapi.json). | Server | URL | | ---------- | ----------------------------------- | | Sandbox | `https://stagingvault.megtrust.com` | | Production | `https://vault.megtrust.com` | Every path starts with `/api/v1/`. Every request needs a fresh Ed25519 signature, and a private key must never enter a browser. Call the API from your server with a signing client — see [Sign a request](/docs/get-started/sign-a-request). ## Authentication [#authentication] The OpenAPI document declares three headers — `x-api-key-id`, `x-api-timestamp` and `x-api-signature` — but a static value in them is not enough: the signature covers the timestamp, method, exact path and query, body hash and idempotency key of each request. Code generated from the document still needs the signing step. Each operation also accepts `_request_id`, a fresh random UUID per attempt that keeps identical requests in the same second distinct. The supplied clients add it for you. ## Import it [#import-it] Import the document into Postman or your code generator, then set the server to your environment. # Environments (/docs/get-started/environments) | Environment | API base URL | Origin (for the client and connector) | | ----------- | ------------------------------------------ | ------------------------------------- | | Sandbox | `https://stagingvault.megtrust.com/api/v1` | `https://stagingvault.megtrust.com` | | Production | `https://vault.megtrust.com/api/v1` | `https://vault.megtrust.com` | The two environments share one API contract and nothing else: workspaces, wallets, destinations, payouts, webhook endpoints and API keys are all separate. ## Separate keys per environment [#separate-keys-per-environment] A key registered in the sandbox console does not work in production, and the reverse. Generate a separate keypair for each environment and each integration, and register each public key in that environment's console under **Developers → API keys**. See [Generate keys](/docs/get-started/generate-keys). Do not infer sandbox versus production from the Key ID. Confirm the host you are calling. ## Base URL versus origin [#base-url-versus-origin] Endpoint paths in these docs start with `/api/v1/`. The supplied [Node.js client](/docs/get-started/first-call) and the [API connector](/docs/ai-tools/api-connector-mcp) take the **origin** only — for example `MEGTRUST_BASE_URL=https://stagingvault.megtrust.com` — and add `/api/v1/...` themselves. Do not append `/api/v1` to `MEGTRUST_BASE_URL`. ## Sandbox funds [#sandbox-funds] The sandbox holds test-network assets with no real value. `usd_value` is still populated there, priced off the matching main network, so you can build displays against it — but treat it as indicative everywhere, and never use it for your own limits. ## Webhooks and status [#webhooks-and-status] Treat `GET /api/v1/withdrawals/{id}` as the source of truth for a payout, and webhooks as a notification that something may have changed. An integration that polls for status works in both environments whether or not a webhook arrives. See [Webhooks](/docs/guides/webhooks). ## Going live [#going-live] Build and test end to end in the sandbox first, then repeat a read-only signed call against production with a separate production key. The [go-live checklist](/docs/guides/go-live-checklist) lists every step. # Your first call (/docs/get-started/first-call) You need Node.js 22 or later, a registered key ([Generate keys](/docs/get-started/generate-keys)), and `megtrust-private.pem` in the working directory. ## Client [#client] Save the signing function from [Sign a request](/docs/get-started/sign-a-request#nodejs) as `call.mjs`. It points at the sandbox; for production set `BASE` to `https://vault.megtrust.com`. ## Use it [#use-it] ```js title="first-call.mjs" import { call } from "./call.mjs"; const { status, body } = await call("GET", "/api/v1/wallets"); console.log(status, body.data); ``` ```sh MEGTRUST_KEY_ID=YOUR_KEY_ID node first-call.mjs ``` A `200` with an empty `data` array still means success — it proves your signing is correct, which is the part worth getting right first. ## Or use the supplied client [#or-use-the-supplied-client] The downloadable [`client.mjs`](/integrations/megtrust/client.mjs) does the same signing with more guard rails: it refuses non-HTTPS origins, paths outside `/api/v1/`, and a payout without an idempotency key. It uses Node's standard library and needs no install. ```sh mkdir megtrust-integration cd megtrust-integration curl --fail --show-error https://docs.megtrust.com/integrations/megtrust/client.mjs --output client.mjs export MEGTRUST_BASE_URL=https://stagingvault.megtrust.com export MEGTRUST_KEY_ID=YOUR_KEY_ID export MEGTRUST_PRIVATE_KEY_PATH=/absolute/path/megtrust-private.pem ``` `MEGTRUST_BASE_URL` is the origin only — do not append `/api/v1`. ```js title="example.mjs" import { clientFromEnv } from "./client.mjs"; const api = clientFromEnv(); const result = await api.request("GET", "/api/v1/wallets"); if (result.status !== 200) throw new Error(JSON.stringify(result)); console.log(result.body.data); ``` Run `node example.mjs`. Each result has `{ status, body, requestId, retryAfter }`. HTTP error responses are returned with their status; transport failures throw. No call is automatically retried. ## Next [#next] * [Receive funds](/docs/guides/receive-funds) — show the right deposit address. * [Send a payout](/docs/guides/send-a-payout) — request, follow and recover a payout. * [Errors](/docs/guides/errors) — the closed set of error codes to branch on. # Generate keys (/docs/get-started/generate-keys) ## Create your keypair [#create-your-keypair] Requests are signed with **Ed25519**. You generate the keypair; the private key never leaves your machine, and MegTrust stores only the public key — which can verify your signatures but can never create one. Even a breach of our systems could not forge a request as you. ```sh # Writes megtrust-private.pem and prints your PUBLIC key: node -e 'const c=require("crypto"),f=require("fs");const{publicKey,privateKey}=c.generateKeyPairSync("ed25519");f.writeFileSync("megtrust-private.pem",privateKey.export({type:"pkcs8",format:"pem"}),{mode:0o600,flag:"wx"});console.log(publicKey.export({type:"spki",format:"der"}).toString("base64"))' ``` The command refuses to overwrite an existing `megtrust-private.pem`, and writes it readable by your user only. Prefer OpenSSL? `openssl genpkey -algorithm ed25519 …` works on OpenSSL 3.x, but macOS ships LibreSSL, which does not support Ed25519 here — use the Node command above. ## Register the public key [#register-the-public-key] Ask your organisation administrator to open **Developers → API keys** in the console of the intended [environment](/docs/get-started/environments). Paste the printed public key to get your **Key ID**. Keep `megtrust-private.pem` in your server's secret manager — never in browser code, a mobile app, a prompt, or version control. ## What a key can reach [#what-a-key-can-reach] * Each key is fixed to **one workspace**, without its child workspaces. * Expiry, revocation and any configured IP allowlist apply to every request. * Use separate keys and private key files for each environment and each integration. Giving each service its own key also means one busy job can't throttle another (see [Rate limits](/docs/guides/rate-limits)). * These keys belong to MegTrust's client API. No other credentials are needed. ## Rotate a key [#rotate-a-key] 1. Generate a new keypair and register the new public key. 2. Switch your integration to the new Key ID and private key. 3. Verify a signed read, for example `GET /api/v1/wallets`. 4. Revoke the old key in the console. ## Legacy HMAC keys [#legacy-hmac-keys] Older integrations may hold an HMAC secret instead of an Ed25519 key. They sign the same canonical string (see [Sign a request](/docs/get-started/sign-a-request)) with HMAC-SHA256, keyed by the issued secret string (do not hex-decode it), and send it as exactly 64 lowercase hex characters. New integrations should use Ed25519. # Sign a request (/docs/get-started/sign-a-request) ## The canonical string [#the-canonical-string] Each request is signed over the timestamp, method, path, body and idempotency key. A captured request can't be replayed elsewhere, after five minutes, or even a second time — each signature is accepted exactly once. ```text title="5 lines, exact order" {timestamp} {METHOD} {path-with-query} {sha256-hex-of-raw-body} {idempotency-key or empty string} ``` ```text title="signature" x-api-signature = base64( Ed25519-sign(private key, canonical) ) ``` The rules, byte for byte: * Join the five lines with a single LF (`\n`) and sign the UTF-8 bytes. * **timestamp** — Unix **seconds**, within ±300 seconds of the server clock. * **METHOD** — uppercase, e.g. `GET`, `POST`. * **path-with-query** — the path and query string exactly as sent, e.g. `/api/v1/transactions?limit=50&_request_id=…`. * **body hash** — lowercase hex SHA-256 of the raw body bytes you send. A request with no body (every `GET`, and `POST /withdrawals/{id}/cancel`) hashes empty bytes. * **idempotency key** — the `Idempotency-Key` header value, or an empty string. When it is empty the canonical string ends with one LF; otherwise there is no trailing LF. * Send the signature as canonical padded standard base64 (not URL-safe base64). ## Headers [#headers] | Header | Notes | | ----------------- | ----------------------------------------------------------------------------- | | `x-api-key-id` | Your Key ID. | | `x-api-timestamp` | Unix SECONDS. Must be within ±5 minutes of our clock. | | `x-api-signature` | Base64 Ed25519 signature of the canonical string. | | `idempotency-key` | Required on `POST /withdrawals` only. Include it in the canonical string too. | Every response carries `Cache-Control: no-store` and an `X-Request-Id`; quote the request ID when you contact support. ## Node.js [#nodejs] This is the complete signing function used in [Your first call](/docs/get-started/first-call): ```js title="call.mjs" import crypto from "node:crypto"; import fs from "node:fs"; // Your private key — generated by you, never sent to MegTrust. const PRIVATE_KEY = crypto.createPrivateKey(fs.readFileSync("megtrust-private.pem")); const KEY_ID = process.env.MEGTRUST_KEY_ID; const BASE = "https://stagingvault.megtrust.com"; export async function call(method, path, body, idempotencyKey) { // A fresh signed query nonce distinguishes identical calls in the same second. const url = new URL(path, BASE); url.searchParams.set("_request_id", crypto.randomUUID()); path = url.pathname + url.search; const payload = body ? JSON.stringify(body) : ""; const timestamp = Math.floor(Date.now() / 1000).toString(); const bodyHash = crypto.createHash("sha256").update(payload).digest("hex"); const idem = idempotencyKey ?? ""; // 5 lines, in this exact order. const canonical = [timestamp, method.toUpperCase(), path, bodyHash, idem].join("\n"); const signature = crypto.sign(null, Buffer.from(canonical), PRIVATE_KEY).toString("base64"); const res = await fetch(BASE + path, { method, headers: { "content-type": "application/json", "x-api-key-id": KEY_ID, "x-api-timestamp": timestamp, "x-api-signature": signature, ...(idem ? { "idempotency-key": idem } : {}), }, ...(payload ? { body: payload } : {}), redirect: "error", signal: AbortSignal.timeout(30_000), }); return { status: res.status, body: await res.json() }; } ``` For production, set `BASE` to `https://vault.megtrust.com`. ## Other languages [#other-languages] Any language with Ed25519 and HTTPS works. Send the four headers above and build the canonical bytes exactly as listed. The same function in Python, with the [`cryptography`](https://cryptography.io) and [`requests`](https://requests.readthedocs.io) packages: ```python title="megtrust.py" import base64, hashlib, json, os, time, uuid import requests # pip install requests cryptography from cryptography.hazmat.primitives.serialization import load_pem_private_key BASE = os.environ.get("MEGTRUST_BASE_URL", "https://stagingvault.megtrust.com") KEY_ID = os.environ["MEGTRUST_KEY_ID"] with open(os.environ.get("MEGTRUST_PRIVATE_KEY_PATH", "megtrust-private.pem"), "rb") as f: PRIVATE_KEY = load_pem_private_key(f.read(), password=None) def call(method, path, body=None, idempotency_key=None): # A fresh signed query nonce on every attempt, retries included. path += ("&" if "?" in path else "?") + f"_request_id={uuid.uuid4()}" payload = b"" if body is None else json.dumps(body).encode() timestamp = str(int(time.time())) # 5 lines, in this exact order. canonical = "\n".join([ timestamp, method.upper(), path, hashlib.sha256(payload).hexdigest(), idempotency_key or "", ]) signature = base64.b64encode(PRIVATE_KEY.sign(canonical.encode())).decode() headers = { "content-type": "application/json", "x-api-key-id": KEY_ID, "x-api-timestamp": timestamp, "x-api-signature": signature, } if idempotency_key: headers["idempotency-key"] = idempotency_key res = requests.request(method, BASE + path, data=payload or None, headers=headers, timeout=30, allow_redirects=False) return res.status_code, res.json() ``` Two things break signatures in every language: * **Hash the bytes you send.** Serialize the body once, hash that string, and send that same string. Letting an HTTP library re-serialize an object after you hashed it changes the bytes. * **Sign the path you send.** If your HTTP library re-encodes the query string, sign the encoded form. ## Timeouts [#timeouts] **Allow at least 10 seconds.** A typical call answers in well under a second, but the first request after a quiet period also has to start the service, which can take a few seconds. A short client timeout will fail on that first call and succeed on the next — set it generously and you will never see it. ## Retries [#retries] **Re-sign every attempt.** A retry needs a fresh signed query nonce and signature — the supplied client adds a random `_request_id` on every attempt, including within the same second. Resending identical bytes is indistinguishable from a replay and is refused with `unauthorized`. No additional query parameter changes a payment's identity. **Retrying a payout is safe.** Reuse the SAME `Idempotency-Key` and you get the original withdrawal back, flagged `idempotent_replay` — never a second one. So if a request times out and you do not know whether it landed, retry it: that is exactly what the key is for. A NEW key on a retry is a NEW withdrawal, so never generate one per attempt — generate it per intended payment. See [Send a payout](/docs/guides/send-a-payout). | Response | What to do | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `429`, `503` | Wait `Retry-After` seconds, then sign a new attempt. | | `500`, `503`, network failure | Retry; for a payout, only with the same `Idempotency-Key` and body. | | `401` | Check the Key ID, expiry, IP allowlist, your clock, the canonical encoding, and that you did not resend a signature. | | Other `4xx` | Correct the request before retrying. | # Cancel a payout (/docs/guides/cancel-a-payout) `POST /api/v1/withdrawals/{id}/cancel` cancels a payout that is still `pending_approval`. * **No body and no idempotency key.** Send no `Idempotency-Key` header. The canonical string hashes empty bytes and ends with an empty fifth line, exactly like a `GET` (see [Sign a request](/docs/get-started/sign-a-request)). * **Safe to repeat.** Calling it again on a cancelled payout returns the same `200` with the payout unchanged. * Only payouts in your key's workspace can be found; anything else is `404 withdrawal_not_found`. ```js const result = await api.request("POST", `/api/v1/withdrawals/${withdrawalId}/cancel`); ``` ## Results [#results] | Response | Meaning | What to do | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | `200` | The payout, with status `cancelled`. Nothing was sent. A repeat call returns the same `200`. | Done. | | `409 withdrawal_not_cancellable` | An approver has already acted, or the payout is past `pending_approval` (`received`, `approved`, `processing`, or a final status). | The payout continues: follow it with `GET /api/v1/withdrawals/{id}`. | | `404 withdrawal_not_found` | No such payout in your workspace. | Check the id and the key's workspace. | | `503 service_unavailable` | The outcome isn't known yet. | Retry the same call — it is safe to repeat. | The `200` body is a standard [withdrawal](/docs/api-reference/get_withdrawal): ```json title="200" { "data": { "id": "5f0c2a9e-7d1b-4c3a-9e2f-6b8d4a1c3e57", "status": "cancelled", "wallet_id": "acct_7f3a…", "asset": "ETH", "amount": "0.25", "destination": "0x03a6c1f2b7e94d58a0c3e1f6b2d4a7c9e8f081b9", "network": "ethereum", "tx_hash": null, "created_at": "2026-09-15T09:02:11.004Z", "updated_at": "2026-09-15T09:05:40.120Z" }, "meta": {} } ``` ## When cancel loses a race [#when-cancel-loses-a-race] An approver may act at the same moment you cancel. If the approval lands first, cancel returns `409 withdrawal_not_cancellable` and the payout carries on. Re-read `GET /api/v1/withdrawals/{id}` for its real state — never assume a `409` means nothing will be sent. ## After a cancel [#after-a-cancel] * `cancelled` is terminal. To pay again, request a **new** payout with a **new** idempotency key. * A `withdrawal.status_changed` [webhook](/docs/guides/webhooks) fires with status `cancelled` if you have webhooks configured. # Destinations (/docs/guides/destinations) Payouts can only go to destinations in your organisation's destination book (the API calls them **counterparties**). A new destination starts pending and is not usable until its ownership is verified. ## List destinations [#list-destinations] `GET /api/v1/counterparties` returns your destination book. Branch on `can_send` — it is the single field that says whether a destination is usable, so you never have to infer the rule from status and verification separately. `can_send` means the destination passed the book's verification gate; other policy checks may still refuse a payout. ## Add a destination [#add-a-destination] `POST /api/v1/counterparties` registers a destination as **pending**. | Field | Type | Notes | | -------------------------- | ------ | ---------------------------------------------------------------------- | | `network` | string | Required. Network id, e.g. `ethereum`. Up to 128 characters. | | `address` | string | Required. Validated for that network's format. Up to 128 characters. | | `counterparty_type` | string | Required. `individual` · `company`. | | `first_name` / `last_name` | string | Required when `individual`. | | `company_name` | string | Required when `company`. | | `wallet_type` | string | Required. `vasp` (a hosted wallet at a service provider) · `unhosted`. | | `vasp_name` | string | Required when `wallet_type` is `vasp`. | | `country` | string | Required. ISO-2, e.g. `SG`. | | `direction` | string | Optional. `send` (default) · `both`. | Names (`first_name`, `last_name`, `company_name`, `vasp_name`, `label`) are up to 120 characters. Unknown fields are refused with `400 validation_failed`. ```json title="body" { "network": "ethereum", "address": "0x03a6…81b9", "counterparty_type": "company", "company_name": "ACME Markets Pte Ltd", "wallet_type": "vasp", "vasp_name": "ACME Digital Custody", "country": "SG", "direction": "send" } ``` ```json title="response" 201 Created { "data": { "id": "cp_5d02…", "label": "ACME Markets Pte Ltd", "network": "ethereum", "network_name": "Ethereum", "address": "0x03a6…81b9", "direction": "send", "status": "pending", "can_send": false, "verified_at": null, "counterparty_type": "company", "company_name": "ACME Markets Pte Ltd", "wallet_type": "vasp", "vasp_name": "ACME Digital Custody", "country": "SG", "created_at": "2026-07-30T09:10:00.000Z" }, "meta": {} } ``` Re-posting an address that is already registered returns the existing entry with `meta.already_exists: true` instead of erroring. It does not edit the entry. ## Verification [#verification] Creating an entry cannot approve or verify it. Complete ownership verification in the console; until then a payout to it is refused with `422 destination_not_whitelisted`. Poll `GET /counterparties` (or check before each payout) and send only when `can_send` is `true`. # Errors (/docs/guides/errors) ```json title="error envelope" { "error": { "code": "validation_failed", "message": "The request body is invalid.", "details": {} } } ``` `details` is present only on some errors, such as `validation_failed`. The set of codes is closed: a new code is only ever added for a new endpoint or behaviour, and an existing code never changes meaning within a version. ## Codes [#codes] | Code | HTTP | Meaning | | ----------------------------- | ---- | ---------------------------------------------------------------------------------------------- | | `unauthorized` | 401 | Unknown, revoked or expired key; bad signature; or a signature already used once. | | `invalid_timestamp` | 401 | Clock more than 5 minutes off, or the timestamp is missing/malformed. Sync NTP. | | `rate_limited` | 429 | Over 120 requests/minute for this key. | | `validation_failed` | 400 | Body or query failed validation (including a malformed `cursor`) — see the `details` field. | | `idempotency_key_required` | 400 | `POST /withdrawals` needs an `Idempotency-Key` header. | | `idempotency_conflict` | 409 | That payment key was used for another payload. Recover the original payment before proceeding. | | `withdrawal_not_cancellable` | 409 | The payout is past `pending_approval`, or an approver acted first. Re-read its status. | | `request_too_large` | 413 | Request bodies are limited to 64 KiB. | | `wallet_not_found` | 404 | No such wallet in your workspace. | | `withdrawal_not_found` | 404 | No such withdrawal in your workspace. | | `transaction_not_found` | 404 | No such transaction in your workspace. | | `asset_not_held` | 422 | That wallet holds no such asset on that network. | | `destination_not_whitelisted` | 422 | The destination is not an approved, verified destination yet. | | `no_approver_available` | 422 | The workspace has no second approver configured. | | `withdrawal_failed` | 422 | The request could not be processed. | | `service_unavailable` | 503 | Temporary service failure. Honour `Retry-After` and preserve the payment key and body. | | `internal_error` | 500 | Something went wrong on our side. Safe to retry with the same idempotency key. | A resource outside your workspace is always a `404`, never a `403`, so a response never confirms whether an id exists elsewhere. ## Retrying [#retrying] | Response | Retry? | | ---------------------- | ----------------------------------------------------------------------------------------------------------------- | | `429`, `503` | Yes, after `Retry-After` seconds, with a newly signed attempt. | | `500`, network failure | Yes; for a payout, only with the same `Idempotency-Key` and body. | | `401` | Not until you fix the cause: credentials, expiry, IP allowlist, clock, canonical encoding, or a resent signature. | | Other `4xx` | No — correct the request first. | See [Sign a request](/docs/get-started/sign-a-request#retries) for why every attempt needs a new signature. # Go-live checklist (/docs/guides/go-live-checklist) ## In the sandbox [#in-the-sandbox] Use the sandbox host (`https://stagingvault.megtrust.com`) and a sandbox key to confirm each item: * [ ] **Key registration** — your public key is registered and you have its Key ID. * [ ] **A signed read** — `GET /api/v1/wallets` returns `200`. * [ ] **A foreign-wallet 404** — `GET /api/v1/wallets/{id}` with an id that is not yours returns `404 wallet_not_found`. * [ ] **Deposit addresses** — your UI shows `deposit_addresses[]` by exact network, never the top-level `address`. * [ ] **Destination** — a destination registered through the API shows `can_send: true` once verified. * [ ] **Payout retry recovery** — a payout retried with the same `Idempotency-Key` returns `meta.idempotent_replay: true`, and you can find it with `GET /withdrawals?idempotency_key=…`. * [ ] **Required approvals** — your organisation's approvers approve a sandbox payout, and you follow it to `completed` with `GET /withdrawals/{id}`. * [ ] **Cancel** — cancelling a payout still `pending_approval` returns `cancelled`; your code handles `409 withdrawal_not_cancellable`. * [ ] **Final settlement** — you store `tx_hash` and never show "sent" on a `202`. * [ ] **Reconciliation** — you page `GET /transactions` with `meta.next_cursor` until it is `null`. * [ ] **Webhook receiver** — deliveries verify, deduplicate by `event.id`, and return `2xx` within 10 seconds. * [ ] **Errors** — your code branches on `error.code`, honours `Retry-After`, and re-signs every attempt. ## In production [#in-production] * [ ] Generate a **separate production keypair** and register it in the production console. * [ ] Repeat the read-only authentication check against `https://vault.megtrust.com` with the production key. * [ ] Register your production webhook receiver and store its secret. The supplied client's own checks do not prove that your deployment's credentials, approval policies, network funding or webhook receiver are configured. Only the steps above do. # Rate limits (/docs/guides/rate-limits) | Limit | Value | When exceeded | | ------------ | ----------------------- | ----------------------- | | Requests | 120 per minute, per key | `429 rate_limited` | | Request body | 64 KiB | `413 request_too_large` | Above 120 requests per minute a key receives `429 rate_limited`. Wait the number of seconds in the `Retry-After` header, then sign a new attempt — a resent signature is refused as a replay. Give each of your services its own key so one busy job can't throttle another — and so you can revoke one without breaking the rest. ## Staying under the limit [#staying-under-the-limit] * Page with `limit=100` when you reconcile: fewer, larger pages. * Prefer [webhooks](/docs/guides/webhooks) to tight polling loops, and poll a payout's status with backoff rather than in a fixed tight interval. * Cache `GET /wallets` and `GET /counterparties` for display; re-read them before acting on money. # Receive funds (/docs/guides/receive-funds) ## List wallets [#list-wallets] `GET /api/v1/wallets` returns every wallet in your workspace, with balances and deposit addresses. No parameters. | Field | Type | Notes | | --------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | | `id` | string | Wallet id — use it as `wallet_id` when requesting a payout. | | `name` | string | Human label. | | `status` | string | `active` · `locked` · `closed`. | | `deposit_addresses[]` | array | One entry per network. Use this to receive funds. | | `networks[]` | array | Networks this wallet supports; `network.id` round-trips into a payout. | | `balances[]` | array | `{ asset, network, amount, available, usd_value }`. `amount` is the total; `available` is spendable. Both are decimal strings. | | `total_usd` | number | Indicative display value from a cached feed — never use it for your own limits. | ```json title="200" { "data": [ { "id": "acct_7f3a…", "name": "Operating Wallet", "status": "active", "address": "0xe8d8…a119", "deposit_addresses": [ { "network": "ethereum", "network_name": "Ethereum", "address": "0xe8d8…a119" }, { "network": "tron", "network_name": "Tron", "address": "TQn9Y2…mHxq" } ], "networks": [ { "id": "ethereum", "name": "Ethereum" }, { "id": "tron", "name": "Tron" } ], "balances": [ { "asset": "ETH", "amount": "12.5", "usd_value": 37500 }, { "asset": "USDT", "amount": "250000", "usd_value": 250000 } ], "total_usd": 287500 } ], "meta": { "count": 1 } } ``` Amounts are decimal strings; keep them as strings or use decimal arithmetic. USD figures are indicative. Never show an unavailable balance as zero — a failed balance read returns an error instead. `GET /api/v1/wallets/{id}` returns one wallet in the same shape. It returns `404 wallet_not_found` for anything outside your workspace — never `403`, so the response never confirms whether an id exists elsewhere. ## Use the right address per network [#use-the-right-address-per-network] Read `deposit_addresses` from `GET /api/v1/wallets` and show the entry matching the network the sender is actually sending on. It is one address chosen for single-line display and is only correct for its own network — paying it on a different chain loses the funds permanently. ## What happens next [#what-happens-next] Incoming funds go through routine checks before they are released to the wallet. If [webhooks](/docs/guides/webhooks) are configured you get two events: | Event | Meaning | | ------------------- | -------------------------------------- | | `deposit.received` | Funds arrived and are being processed. | | `deposit.completed` | The deposit is settled and spendable. | The movement appears in [`GET /api/v1/transactions`](/docs/guides/reconcile-transactions) either way. Never credit a deposit to your customer before it is completed. # Reconcile transactions (/docs/guides/reconcile-transactions) `GET /api/v1/transactions` lists on-chain movements in and out of your wallets, newest first. Page through it with a cursor to reconcile your ledger against ours. The transactions list holds on-chain movements only. A payout that is not on the network yet — for example one waiting for approval — is not in it. Follow payouts with [`GET /api/v1/withdrawals`](/docs/guides/send-a-payout#follow-the-lifecycle). ## Query parameters [#query-parameters] | Parameter | Type | Notes | | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------- | | `limit` | integer | 1–100. Default 25. | | `cursor` | string | Opaque. Pass `meta.next_cursor` from the previous page. A malformed cursor returns `400 validation_failed`. | | `created_after` | ISO 8601 | Optional range filter: only movements **recorded** after this time. | | `created_before` | ISO 8601 | Optional range filter: only movements **recorded** before this time. | | `wallet_id` | string | Optional. Restrict to one wallet. | A call without `cursor`, `created_after` or `created_before` returns the newest page. The range filters apply to the time we recorded the movement, which can be well after its block time. A movement mined just before a boundary can therefore land in the next range. Reconcile consecutive ranges back to back and every movement falls in exactly one of them. ## Response [#response] | Field | Type | Notes | | -------------------------------------- | ------------ | -------------------------------------------------------------------------------------- | | `id` | string | Transaction id. A move between two of your own wallets is two rows with the same `id`. | | `direction` | string | `in` · `out` · `internal`. | | `status` | string | Normalised stage, e.g. `completed`, `broadcasting`. | | `tx_hash` | string\|null | Null until the transfer reaches the network. | | `from` / `to` | object | `{ name, address }`. `name` is null for an outside party. | | `amount`, `asset` | string | Decimal string and asset symbol. | | `fee`, `fee_asset` | string\|null | The network fee, folded into its payout row. | | `network`, `network_name`, `wallet_id` | string\|null | Where the movement happened. | | `meta.next_cursor` | string\|null | Cursor for the next (older) page; `null` on the last page. | ```json title="200" { "data": [ { "id": "9c21d0e4-3b7a-4f58-a1c6-2e8f7b90d4a3", "created_at": "2026-09-15T08:31:21.823Z", "direction": "out", "from": { "name": "Operating Wallet", "address": "0xe8d8…a119" }, "to": { "name": null, "address": "0x03a6…81b9" }, "amount": "0.01", "asset": "ETH", "fee": "0.0000467", "fee_asset": "ETH", "status": "completed", "tx_hash": "0xfd31…e730", "network": "ethereum", "network_name": "Ethereum", "wallet_id": "acct_7f3a…" } ], "meta": { "count": 1, "limit": 25, "next_cursor": null } } ``` A read that can't be completed fails with an error — for example `503 service_unavailable` — never with a partial page. Retry it with a newly signed request. ## Page with the cursor [#page-with-the-cursor] Follow `meta.next_cursor` until it is `null`. That is the only reliable end-of-list signal: * A page may hold slightly **fewer** rows than `limit`, because network fees fold into their payout row. * A page may hold **more** rows than `limit`, because a move between two of your own wallets is two rows (an `out` and an `in`). So never stop because a page came back short, and never page by moving `created_before` yourself — `created_after` and `created_before` are range filters, not a paging mechanism. To reconcile a period, set the range once and keep it on every request while you follow the cursor. ```js // Every movement recorded in September 2026, newest first. const range = { created_after: "2026-09-01T00:00:00Z", created_before: "2026-10-01T00:00:00Z" }; let cursor = null; do { const query = new URLSearchParams({ ...range, limit: "100", ...(cursor ? { cursor } : {}) }); const { status, body } = await api.request("GET", `/api/v1/transactions?${query}`); if (status !== 200) throw new Error(JSON.stringify(body)); for (const row of body.data) upsert(`${row.id}:${row.direction}`, row); // idempotent write cursor = body.meta.next_cursor; } while (cursor); ``` Store rows keyed by `id` **and** `direction`: the two legs of an internal move share an `id`. Writing idempotently means a re-run of the same range is harmless. ## Get one transaction [#get-one-transaction] `GET /api/v1/transactions/{id}` returns `data` as an **array of legs**: * one leg for a deposit or a payout; * two legs — an `out` and an `in` — for a move between two of your own wallets. Each leg has the same fields as a list row. An id outside your workspace returns `404 transaction_not_found`. ```json title="200" { "data": [ { "id": "9c21d0e4-3b7a-4f58-a1c6-2e8f7b90d4a3", "created_at": "2026-09-15T08:31:21.823Z", "direction": "out", "from": { "name": "Operating Wallet", "address": "0xe8d8…a119" }, "to": { "name": null, "address": "0x03a6…81b9" }, "amount": "0.01", "asset": "ETH", "fee": "0.0000467", "fee_asset": "ETH", "status": "completed", "tx_hash": "0xfd31…e730", "network": "ethereum", "network_name": "Ethereum", "wallet_id": "acct_7f3a…" } ], "meta": { "count": 1 } } ``` # Send a payout (/docs/guides/send-a-payout) ## Before you start [#before-you-start] * The destination must already be an approved, verified destination — see [Destinations](/docs/guides/destinations). * Take `wallet_id`, `asset` and `network` from the wallet's balances in `GET /api/v1/wallets`. * Obtain the payment instruction and **persist one idempotency key for this payment** before the first attempt — for example your invoice or payout reference. ## Request a withdrawal [#request-a-withdrawal] `POST /api/v1/withdrawals` submits a withdrawal into your organisation's approval flow. Every payout passes compliance checks and a human approval before it reaches the network. Neither the API nor the MCP connector exposes an approval endpoint. Never show a user "sent" on a 202 — poll the status endpoint or wait for the webhook. | Field | Type | Notes | | ----------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `wallet_id` | string | Required. From `GET /wallets`. Up to 128 characters. | | `asset` | string | Required. Symbol, e.g. `ETH`. Must be held by that wallet. | | `amount` | string | Required. Positive decimal STRING, e.g. `"0.25"`, up to 100 characters. More decimals than the asset allows returns `400`. | | `destination` | string | Required. Must already be an approved, verified destination. Up to 128 characters. | | `network` | string | Use the exact balance network. Required when the asset is held on several networks; if omitted, it resolves to the one network the asset is held on. | | `fee_priority` | string | Optional. `low` · `medium` · `high`. | | `idempotency-key` | header | Required. Retry with the SAME value to avoid a second withdrawal. | Bodies reject unknown fields with `400`. Idempotency keys are 1–128 printable ASCII characters without spaces, and are unique within your organisation. A withdrawal is visible only to keys of the workspace that created it. ```json title="body" { "wallet_id": "acct_7f3a…", "asset": "ETH", "amount": "0.25", "destination": "0x03a6…81b9", "network": "ethereum" } ``` ```json title="response" 202 Accepted { "data": { "id": "5f0c2a9e-7d1b-4c3a-9e2f-6b8d4a1c3e57", "status": "pending_approval", "wallet_id": "acct_7f3a…", "asset": "ETH", "amount": "0.25", "destination": "0x03a6…81b9", "network": "ethereum", "tx_hash": null, "created_at": "2026-07-30T09:02:11.004Z", "updated_at": "2026-07-30T09:02:11.004Z" }, "meta": {} } ``` With the supplied [Node.js client](/docs/get-started/first-call#or-use-the-supplied-client): ```js // These values come from your persisted payment record. const paymentKey = "invoice-2026-0042"; const payment = { wallet_id: "WALLET_ID_FROM_API", asset: "USDC", amount: "25.50", network: "ethereum", destination: "VERIFIED_DESTINATION_ADDRESS", fee_priority: "medium" }; const result = await api.request("POST", "/api/v1/withdrawals", payment, paymentKey); if (result.status === 202 || result.status === 200) { const status = await api.request("GET", `/api/v1/withdrawals/${result.body.data.id}`); console.log(status.body); } ``` ## Follow the lifecycle [#follow-the-lifecycle] `GET /api/v1/withdrawals/{id}` refreshes progress through settlement. Read `data.status` even on a successful HTTP response. ```text received → pending_approval → approved → processing → completed ``` | Status | Meaning | | --------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `received` | Accepted by us, not yet submitted for approval. | | `pending_approval` | Pending approval or processing checks. The only state you can [cancel](/docs/guides/cancel-a-payout) from. | | `approved` | Approved, preparing to send. | | `processing` | Submitted to the network. | | `completed` | Settled. `tx_hash` is populated. | | `rejected` · `failed` · `expired` | Terminal unsuccessful outcome. Investigate before requesting another payment. | | `cancelled` | Terminal. You cancelled it before approval; nothing was sent. | If a live refresh fails, the response carries the last recorded state with `meta.stale: true`. Final states come from the record. Polling never approves or retries a payment. Every status change also arrives as a `withdrawal.status_changed` [webhook](/docs/guides/webhooks) if you have one configured. Treat the webhook as a prompt to re-read `GET /withdrawals/{id}`, which is the source of truth. ## Retry safely [#retry-safely] If a `POST` times out or fails with `500`/`503` or a network error, retry with the **same key and the same body**. The client signs a fresh attempt. You get one of: * `200` with `meta.idempotent_replay: true` and the original withdrawal — including a recorded `failed` or still-`received` status. A replay returns recorded state; it never resumes or reissues the payment. * `202` — the earlier attempt never landed, so this one is evaluated afresh. * `409 idempotency_conflict` — that key was already used for a different body. Recover the original payment before proceeding. A collision with a payment outside your workspace also returns a conflict, without exposing that payment. A request refused **before it is accepted** — a `400`, a `404`, a `422` for the wallet or asset, or a `503` — records nothing. Fix the cause if there is one, then retry with the same key: the retry is evaluated afresh, as if it were the first attempt. Only the same key with a **different** body is refused, with `409 idempotency_conflict`. A NEW key on a retry is a NEW withdrawal. Never generate a replacement key for an uncertain payment. ## Recover a lost submission [#recover-a-lost-submission] `GET /api/v1/withdrawals` lists recorded payouts, including a submission whose response was lost. | Parameter | Type | Notes | | ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------- | | `limit` | integer | 1–100; default 25. | | `offset` | integer | 0–10000; default 0. Follow `meta.next_offset` until null. Deduplicate by id when new payments arrive during paging. | | `wallet_id` | string | Optional wallet filter within this key's workspace. | | `idempotency_key` | string | Look up the original payment after a timeout. Preserve its key and body when retrying. | ```js const found = await api.request("GET", `/api/v1/withdrawals?idempotency_key=${encodeURIComponent(paymentKey)}`); ``` Listing returns recorded state. Use `GET /withdrawals/{id}` to refresh an individual payout. If a payout stays `received` or otherwise unresolved, contact your administrator with the withdrawal ID and the `X-Request-Id` of your request. ## Errors you should handle [#errors-you-should-handle] | Code | HTTP | Meaning | | ----------------------------- | ---- | ------------------------------------------------------------- | | `idempotency_key_required` | 400 | `POST /withdrawals` needs an `Idempotency-Key` header. | | `idempotency_conflict` | 409 | That payment key was used for another payload. | | `asset_not_held` | 422 | That wallet holds no such asset on that network. | | `destination_not_whitelisted` | 422 | The destination is not an approved, verified destination yet. | | `no_approver_available` | 422 | The workspace has no second approver configured. | | `withdrawal_failed` | 422 | The request could not be processed. | The full list is on [Errors](/docs/guides/errors). # Webhooks (/docs/guides/webhooks) Webhooks tell you that something may have changed. The API stays the source of truth: re-read `GET /api/v1/withdrawals/{id}` for a payout and `GET /api/v1/transactions` for movements, and use polling for recovery rather than relying on events alone. ## Register a receiver [#register-a-receiver] Register a public HTTPS receiver in the console under **Developers → Webhooks**, and save its one-time secret separately from your API key. ## Events [#events] Every delivery has the same envelope: `{ id, type, created_at, data }`. | Event | Meaning | | --------------------------- | ------------------------------------------------------- | | `withdrawal.status_changed` | A payout moved. `data` matches `GET /withdrawals/{id}`. | | `deposit.received` | Funds arrived and are being processed. | | `deposit.completed` | The deposit is settled and spendable. | ```json title="example delivery" { "id": "evt_dep_9c21…_Released", "type": "deposit.completed", "created_at": "2026-07-30T09:20:00.000Z", "data": { "id": "9c21…", "status": "completed", "wallet_id": "acct_7f3a…", "asset": "USDT", "amount": "500", "from_address": "0xfa11…cee7", "tx_hash": "0x24de…abcd", "received_at": "2026-07-30T09:18:42.000Z" } } ``` The event `id` is stable per subject and state, so it is a safe deduplication key for your handler. The full event schemas are in the [API reference](/docs/api-reference). ## Verify every delivery [#verify-every-delivery] Check the signature before trusting a payload. Use the **raw** body — a re-serialized object will not match. | Header | Notes | | --------------------- | --------------------------------------------------------------------------------------- | | `x-webhook-timestamp` | Unix seconds. Reject if more than 5 minutes old. | | `x-webhook-signature` | Lowercase hex HMAC-SHA256 over `timestamp + "." + rawBody`, using your endpoint secret. | ```js title="verify.mjs" import crypto from "node:crypto"; // Verify a webhook delivery. Use the RAW request body — a re-serialized // object will not match. export function verify(rawBody, headers, secret) { const timestamp = headers["x-webhook-timestamp"]; const signature = headers["x-webhook-signature"]; if (!timestamp || !signature) return false; // Reject anything older than 5 minutes (replay protection). if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const expected = crypto.createHmac("sha256", secret) .update(timestamp + "." + rawBody).digest("hex"); const a = Buffer.from(expected, "hex"); const b = Buffer.from(signature, "hex"); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` The supplied [`client.mjs`](/integrations/megtrust/client.mjs) exports the same check as `verifyWebhook`: ```js import { verifyWebhook } from "./client.mjs"; const valid = verifyWebhook(rawBody, request.headers.get("x-webhook-timestamp"), request.headers.get("x-webhook-signature"), process.env.MEGTRUST_WEBHOOK_SECRET); if (!valid) return new Response("Invalid signature", { status: 401 }); const event = JSON.parse(rawBody.toString("utf8")); // Durably record event.id and queue processing in one transaction, ignoring duplicates. // Return 2xx only after that write succeeds; process the queue outside the request. ``` ## Handle deliveries safely [#handle-deliveries-safely] * Deliveries may be duplicated, delayed or arrive out of order. Deduplicate by `event.id`. * Record the event and return `2xx` quickly — within 10 seconds — then process it outside the request. * For a payout, re-read `GET /withdrawals/{id}` before acting on a status. * Never credit an unsettled deposit as completed. ## Retries [#retries] Anything other than a `2xx` is retried with exponential backoff: up to eight attempts over about eight hours. After that the delivery is left recorded as failed — visible to you on the console's **Webhooks** tab with its last error. # List workspace wallets (/docs/api-reference/list_wallets) Balances are decimal strings and identify the network. Unreadable balances return an error instead of zero. ## GET /api/v1/wallets ```json { "operationId": "list_wallets", "summary": "List workspace wallets", "responses": { "200": { "description": "List workspace wallets", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WalletListResponse" } } } }, "400": { "description": "validation_failed, idempotency_key_required", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "unauthorized, invalid_timestamp", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "wallet_not_found, withdrawal_not_found", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "idempotency_conflict", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "413": { "description": "request_too_large", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "asset_not_held, destination_not_whitelisted, no_approver_available, withdrawal_failed", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "rate_limited", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "internal_error", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "service_unavailable", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "description": "Balances are decimal strings and identify the network. Unreadable balances return an error instead of zero." } ``` Schemas referenced with `$ref` are in the full OpenAPI document: /integrations/megtrust/openapi.json (or the `get_openapi` tool on the docs MCP server). # Read one workspace wallet (/docs/api-reference/get_wallet) ## GET /api/v1/wallets/{id} ```json { "operationId": "get_wallet", "summary": "Read one workspace wallet", "responses": { "200": { "description": "Read one workspace wallet", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WalletResponse" } } } }, "400": { "description": "validation_failed, idempotency_key_required", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "unauthorized, invalid_timestamp", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "wallet_not_found, withdrawal_not_found", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "idempotency_conflict", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "413": { "description": "request_too_large", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "asset_not_held, destination_not_whitelisted, no_approver_available, withdrawal_failed", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "rate_limited", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "internal_error", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "service_unavailable", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } ``` Schemas referenced with `$ref` are in the full OpenAPI document: /integrations/megtrust/openapi.json (or the `get_openapi` tool on the docs MCP server). # Read recent movements (/docs/api-reference/list_transactions) On-chain movements, newest first, cursor-paged: follow meta.next_cursor until it is null. A page can hold slightly fewer rows than limit (network fees fold into their payout row) or more (a move between two of your own wallets is two rows). Payouts waiting for approval are not listed here; follow them in /api/v1/withdrawals. ## GET /api/v1/transactions ```json { "operationId": "list_transactions", "summary": "Read recent movements", "responses": { "200": { "description": "Read recent movements", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TransactionListResponse" } } } }, "400": { "description": "validation_failed, idempotency_key_required", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "unauthorized, invalid_timestamp", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "wallet_not_found, withdrawal_not_found", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "idempotency_conflict", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "413": { "description": "request_too_large", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "asset_not_held, destination_not_whitelisted, no_approver_available, withdrawal_failed", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "rate_limited", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "internal_error", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "service_unavailable", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "parameters": [ { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } }, { "name": "wallet_id", "in": "query", "required": false, "schema": { "type": "string", "minLength": 1, "maxLength": 128 } }, { "name": "cursor", "in": "query", "required": false, "schema": { "type": "string", "format": "uuid" }, "description": "meta.next_cursor from the previous page. Page with this only." }, { "name": "created_after", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "Only movements recorded after this time. A range filter, not a paging mechanism." }, { "name": "created_before", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "Only movements recorded before this time. A range filter, not a paging mechanism." } ], "description": "On-chain movements, newest first, cursor-paged: follow meta.next_cursor until it is null. A page can hold slightly fewer rows than limit (network fees fold into their payout row) or more (a move between two of your own wallets is two rows). Payouts waiting for approval are not listed here; follow them in /api/v1/withdrawals." } ``` Schemas referenced with `$ref` are in the full OpenAPI document: /integrations/megtrust/openapi.json (or the `get_openapi` tool on the docs MCP server). # Read one movement (/docs/api-reference/get_transaction) Every leg of one movement, as an array: one for a deposit or payout, two for a move between two of your own wallets (list rows already share the id). 404 transaction_not_found for anything outside your workspace. ## GET /api/v1/transactions/{id} ```json { "operationId": "get_transaction", "summary": "Read one movement", "description": "Every leg of one movement, as an array: one for a deposit or payout, two for a move between two of your own wallets (list rows already share the id). 404 transaction_not_found for anything outside your workspace.", "responses": { "200": { "description": "Read one movement", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TransactionListResponse" } } } }, "401": { "description": "unauthorized, invalid_timestamp", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "transaction_not_found", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "rate_limited", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "internal_error", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "service_unavailable", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } ``` Schemas referenced with `$ref` are in the full OpenAPI document: /integrations/megtrust/openapi.json (or the `get_openapi` tool on the docs MCP server). # List destinations (/docs/api-reference/list_counterparties) ## GET /api/v1/counterparties ```json { "operationId": "list_counterparties", "summary": "List destinations", "responses": { "200": { "description": "List destinations", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CounterpartyListResponse" } } } }, "400": { "description": "validation_failed, idempotency_key_required", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "unauthorized, invalid_timestamp", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "wallet_not_found, withdrawal_not_found", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "idempotency_conflict", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "413": { "description": "request_too_large", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "asset_not_held, destination_not_whitelisted, no_approver_available, withdrawal_failed", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "rate_limited", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "internal_error", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "service_unavailable", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } ``` Schemas referenced with `$ref` are in the full OpenAPI document: /integrations/megtrust/openapi.json (or the `get_openapi` tool on the docs MCP server). # Register a pending destination (/docs/api-reference/create_counterparty) ## POST /api/v1/counterparties ```json { "operationId": "create_counterparty", "summary": "Register a pending destination", "responses": { "200": { "description": "Existing destination; meta.already_exists=true. Does not edit it.", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CounterpartyResponse" } } } }, "201": { "description": "Register a pending destination", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CounterpartyResponse" } } } }, "400": { "description": "validation_failed, idempotency_key_required", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "unauthorized, invalid_timestamp", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "wallet_not_found, withdrawal_not_found", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "idempotency_conflict", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "413": { "description": "request_too_large", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "asset_not_held, destination_not_whitelisted, no_approver_available, withdrawal_failed", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "rate_limited", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "internal_error", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "service_unavailable", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CounterpartyRequest" } } } } } ``` Schemas referenced with `$ref` are in the full OpenAPI document: /integrations/megtrust/openapi.json (or the `get_openapi` tool on the docs MCP server). # List recorded withdrawals (/docs/api-reference/list_withdrawals) Scoped to this key's workspace. Follow meta.next_offset (null at the end); new rows may shift offsets, so deduplicate by id. Use the detail endpoint to refresh status. ## GET /api/v1/withdrawals ```json { "operationId": "list_withdrawals", "summary": "List recorded withdrawals", "responses": { "200": { "description": "List recorded withdrawals", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WithdrawalListResponse" } } } }, "400": { "description": "validation_failed, idempotency_key_required", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "unauthorized, invalid_timestamp", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "wallet_not_found, withdrawal_not_found", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "idempotency_conflict", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "413": { "description": "request_too_large", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "asset_not_held, destination_not_whitelisted, no_approver_available, withdrawal_failed", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "rate_limited", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "internal_error", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "service_unavailable", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "parameters": [ { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 25 } }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "maximum": 10000, "default": 0 } }, { "name": "wallet_id", "in": "query", "required": false, "schema": { "type": "string", "minLength": 1, "maxLength": 128 } }, { "name": "idempotency_key", "in": "query", "required": false, "schema": { "type": "string", "minLength": 1, "maxLength": 128 } } ], "description": "Scoped to this key's workspace. Follow meta.next_offset (null at the end); new rows may shift offsets, so deduplicate by id. Use the detail endpoint to refresh status." } ``` Schemas referenced with `$ref` are in the full OpenAPI document: /integrations/megtrust/openapi.json (or the `get_openapi` tool on the docs MCP server). # Request a withdrawal under organisation policy (/docs/api-reference/request_withdrawal) 202 is accepted for processing, never proof of settlement. One persisted idempotency key per payment, unique within the organisation. Same key and validated body return the original row (200); a conflicting body returns 409. Replays do not reissue failed or received rows. No approval endpoint is exposed. ## POST /api/v1/withdrawals ```json { "operationId": "request_withdrawal", "summary": "Request a withdrawal under organisation policy", "responses": { "200": { "description": "Original withdrawal; meta.idempotent_replay=true.", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WithdrawalResponse" } } } }, "202": { "description": "Request a withdrawal under organisation policy", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WithdrawalResponse" } } } }, "400": { "description": "validation_failed, idempotency_key_required", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "unauthorized, invalid_timestamp", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "wallet_not_found, withdrawal_not_found", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "idempotency_conflict", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "413": { "description": "request_too_large", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "asset_not_held, destination_not_whitelisted, no_approver_available, withdrawal_failed", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "rate_limited", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "internal_error", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "service_unavailable", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "parameters": [ { "name": "Idempotency-Key", "in": "header", "required": true, "schema": { "type": "string", "pattern": "^[\\x21-\\x7e]{1,128}$" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WithdrawalRequest" } } } }, "description": "202 is accepted for processing, never proof of settlement. One persisted idempotency key per payment, unique within the organisation. Same key and validated body return the original row (200); a conflicting body returns 409. Replays do not reissue failed or received rows. No approval endpoint is exposed." } ``` Schemas referenced with `$ref` are in the full OpenAPI document: /integrations/megtrust/openapi.json (or the `get_openapi` tool on the docs MCP server). # Refresh withdrawal status (/docs/api-reference/get_withdrawal) Returns last recorded state with meta.stale=true if a live refresh fails. Final rows use recorded state. Polling never approves or retries a payment. ## GET /api/v1/withdrawals/{id} ```json { "operationId": "get_withdrawal", "summary": "Refresh withdrawal status", "responses": { "200": { "description": "Refresh withdrawal status", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WithdrawalResponse" } } } }, "400": { "description": "validation_failed, idempotency_key_required", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "unauthorized, invalid_timestamp", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "wallet_not_found, withdrawal_not_found", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "idempotency_conflict", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "413": { "description": "request_too_large", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "422": { "description": "asset_not_held, destination_not_whitelisted, no_approver_available, withdrawal_failed", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "rate_limited", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "internal_error", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "service_unavailable", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } }, "description": "Returns last recorded state with meta.stale=true if a live refresh fails. Final rows use recorded state. Polling never approves or retries a payment." } ``` Schemas referenced with `$ref` are in the full OpenAPI document: /integrations/megtrust/openapi.json (or the `get_openapi` tool on the docs MCP server). # Cancel a pending withdrawal (/docs/api-reference/cancel_withdrawal) Withdraws a payout that is still pending_approval. No body and no Idempotency-Key; repeating it is safe and returns the cancelled withdrawal. 409 withdrawal_not_cancellable once an approver has acted or the payout has moved past pending_approval. ## POST /api/v1/withdrawals/{id}/cancel ```json { "operationId": "cancel_withdrawal", "summary": "Cancel a pending withdrawal", "description": "Withdraws a payout that is still pending_approval. No body and no Idempotency-Key; repeating it is safe and returns the cancelled withdrawal. 409 withdrawal_not_cancellable once an approver has acted or the payout has moved past pending_approval.", "responses": { "200": { "description": "Cancel a pending withdrawal", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WithdrawalResponse" } } } }, "401": { "description": "unauthorized, invalid_timestamp", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "withdrawal_not_found", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "409": { "description": "withdrawal_not_cancellable", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "429": { "description": "rate_limited", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "500": { "description": "internal_error", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "503": { "description": "service_unavailable", "headers": { "X-Request-Id": { "description": "Server-generated support correlation ID.", "schema": { "type": "string" } }, "Cache-Control": { "schema": { "type": "string", "const": "no-store" } }, "Retry-After": { "description": "Seconds to wait before a freshly signed retry.", "schema": { "type": "string", "pattern": "^\\d+$" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } ``` Schemas referenced with `$ref` are in the full OpenAPI document: /integrations/megtrust/openapi.json (or the `get_openapi` tool on the docs MCP server). # Delivery to your registered HTTPS receiver (/docs/api-reference/clientevent) Verify raw bytes with the endpoint secret: HMAC-SHA256(timestamp + '.' + raw body), ±5 minutes. Durably deduplicate event.id. Deliveries may be delayed, duplicated and out of order; up to eight attempts. Respond within 10 seconds after durably accepting the event. Schemas referenced with `$ref` are in the full OpenAPI document: /integrations/megtrust/openapi.json (or the `get_openapi` tool on the docs MCP server).