Developers
The same API that powers this app. JSON over HTTPS; public endpoints need no key.
Endpoints
The main endpoints are below. Every endpoint, schema, error code and webhook payload is in /api/openapi.json (OpenAPI 3.1), which you can load into Postman, Insomnia or a client generator.
GET/api/assets
Supported assets with live USD price and per-order min/max in asset units.
curl -s https://test.tacet.dea.web.id/api/assetsPOST/api/quote
Estimate a swap and get a quoteId: amount out, fee lines in USD, ETA range in minutes and validUntil (about 2 minutes). Send amountOut instead of amount to ask how much to send to receive a given amount (reverse quote; amountOut in the result is at least the target). Requests without demo follow the server mode. Optional rateType: "fixed" guarantees amountOut (a rate-guarantee fee line is added) if exactly amountIn arrives within the deposit window; otherwise the order continues at the floating rate. Fixed is only offered with Express liquidity (422 FIXED_RATE_UNAVAILABLE).
curl -s -X POST https://test.tacet.dea.web.id/api/quote \ -H 'content-type: application/json' \ -d '{"from":"BTC","to":"ETH","amount":0.01}'POST/api/order
Create an order from a valid quoteId. Returns 201 with the order ID and a deposit address unique to the order. Destination and refund addresses are required. Sending the same quoteId again returns the same order (200, replayed: true) instead of a second one. If the rate dropped more than 1% since the quote (0.3% for fixed rate), you get 409 RATE_MOVED with a new quoteId to confirm. With recipients, each one gets its own guaranteed amount (split.legs[].fixedAmountOut). The order carries rateType and fixedRate {amountOut, status: locked | voided | settled}. Optional payoutDelay: true adds a random delay before payout.
curl -s -X POST https://test.tacet.dea.web.id/api/order \ -H 'content-type: application/json' \ -d '{"quoteId":"q_…", "destinationAddress":"0x…","refundAddress":"bc1…"}'GET/api/order/:id
Order status: displayStatus (stable contract, see below), per-leg status, settlement confirmations, payout or refund transaction IDs.
curl -s https://test.tacet.dea.web.id/api/order/aB3dE5fG7hJ9GET/api/order/:id/stream
Server-Sent Events. Sends the order once, then again on every change (typically within half a second). Ends with an end event: reason final when the order is finished, or reconnect after 10 minutes. Open streams are capped per client (behind a trusted proxy) and in total.
curl -N https://test.tacet.dea.web.id/api/order/aB3dE5fG7hJ9/streamGET/api/order/:id/receipt
Ed25519-signed receipt for a COMPLETED or REFUNDED order (409 RECEIPT_NOT_READY before that). Verify it against /api/receipt-key without trusting the channel that delivered it. Users can check a downloaded receipt in their browser at /verify.
curl -s https://test.tacet.dea.web.id/api/order/aB3dE5fG7hJ9/receipt curl -s https://test.tacet.dea.web.id/api/receipt-keyPOST/api/paylinks
Create a payment link: {to, amount, address, memo?, description?}. Payers pay with any token via /api/paylinks/:id/quote {from} and /api/paylinks/:id/order {quoteId, refundAddress}, with the same quote rules as /api/order (a used quoteId returns the existing order; RATE_MOVED carries a new quoteId). The recipient address is never returned to anyone.
curl -s -X POST https://test.tacet.dea.web.id/api/paylinks \ -H 'content-type: application/json' \ -d '{"to":"USDT-TRX","amount":100,"address":"T…"}'GET/api/partner/me
With your API key: orders created with it, completed volume, recent orders. POST /api/partner/webhook {url} sets your webhook; POST /api/partner/webhook/test sends a signed test.ping.
curl -s https://test.tacet.dea.web.id/api/partner/me \ -H 'authorization: Bearer sk_…'POST/api/address/validate
Full checksum validation of a destination or refund address for an asset.
curl -s -X POST https://test.tacet.dea.web.id/api/address/validate \ -H 'content-type: application/json' \ -d '{"asset":"BTC","address":"bc1…"}'GET/api/routes/onchain
Optional on-chain route through LI.FI for EVM assets (also POST with a JSON body). Not private: both ends are linked on-chain. Unsupported pairs return 200 with available: false and a reason. LI.FI on-chain routes earn your markup via LI.FI integrator fees.
curl -s 'https://test.tacet.dea.web.id/api/routes/onchain?from=USDC-ETH&to=BNB&amount=100'GET/api/status
System status: network, router heartbeat, settlement layer height, venue market data and order counts. For probes use /api/livez (process up, always 200) and /api/readyz (503 when the router heartbeat is stale or storage isn't writable).
curl -s https://test.tacet.dea.web.id/api/status
Integrators
API keys. Send Authorization: Bearer sk_… to get per-key limits (300 quotes, 60 orders and 1,200 lookups per minute) and webhooks. Keys are issued by the operator and stored only as a hash; a revoked or wrong key gets 401 INVALID_API_KEY.
Idempotency. Add an Idempotency-Key header (8–128 characters) to POST /api/order or POST /api/paylinks/:id/order. Retrying with the same key and body returns the original response with idempotent-replay: true; the same key with a different body is rejected with IDEMPOTENCY_KEY_REUSED. Keys are remembered for 24 hours.
Webhooks. If your key has a webhook URL (public https only), Tacet POSTs order.created and order.updated events whenever an order's displayStatus changes. Payloads carry the order ID, statuses and transaction IDs — never addresses. Failed deliveries are retried up to 8 times over about 31 hours with backoff. Respond with any 2xx. Verify every request:
// Header: X-Webhook-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, rawBody, header, toleranceSec = 300) {
const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > toleranceSec) return false;
const want = createHmac("sha256", secret).update(`${m[1]}.${rawBody}`).digest();
return timingSafeEqual(want, Buffer.from(m[2], "hex"));
}Embed
Put the swap widget on your site with an iframe. /embed renders only the widget; the order page stays inside the iframe at /embed/order/:id, so the user sees the deposit address without leaving your page or being caught by a popup blocker. Preselect with query parameters: from and to (asset IDs from /api/assets), amount, mode (swap or send) and rate (floating or fixed). Unknown values are ignored.
Allowed origins. Framing is blocked by default. The operator adds your exact origin (scheme, host and port, e.g. https://partner.example) to the allowlist; every other page of Tacet refuses to be framed.
<iframe
src="https://test.tacet.dea.web.id/embed?from=BTC&to=ETH&amount=0.01"
title="Tacet swap"
width="100%" height="760"
style="border:0;border-radius:12px"
></iframe>Events. When the user creates an order, the widget posts {"type": "tacet:order_created", "orderId": "…"} to the parent window, addressed to your allowed origin only (never "*"). Check event.origin before trusting it, then follow the order with GET /api/order/:id or your webhook. The message carries no addresses or amounts.
window.addEventListener("message", (event) => {
if (event.origin !== "https://test.tacet.dea.web.id") return; // only trust messages from Tacet
if (event.data?.type === "tacet:order_created") {
console.log("order created", event.data.orderId);
}
});AI agents (MCP)
Tacet ships a Model Context Protocol server with seven tools: get_assets, get_quote, validate_address, create_order, get_order, wait_for_order and get_receipt. create_order only runs with an explicit confirm: true and reuses the quote as its idempotency key, so a retry never creates a second order. wait_for_order follows the live stream, so the agent waits instead of asking the user to check back. The agent never handles private keys: the user sends the deposit from their own wallet. Run it with npm run mcp and set SWAP_API_URL (and optionally SWAP_API_KEY).
Order status
Read displayStatus. Its values and meanings won't change. Terminal values are COMPLETED, REFUNDED, EXPIRED and FAILED. hops.hop1 and hops.hop2 are pending, in_progress, done or failed.
- WAITING_FOR_DEPOSIT
- Order created; waiting for your deposit.
- DEPOSIT_DETECTED
- Deposit seen on-chain; waiting for confirmations.
- EXCHANGING_SOURCE
- Leg 1: your deposit is being converted at the first liquidity venue.
- PRIVATE_SETTLEMENT
- Funds are moving through private settlement to the second liquidity venue.
- EXCHANGING_TO_DESTINATION
- Leg 2: converting to your destination asset.
- SENDING
- Payout to your destination address is in progress.
- COMPLETED
- Done. The payout transaction ID is in progress.payoutTxid.
- REFUNDING
- The swap stopped; your deposit is being returned to the refund address.
- REFUNDED
- Refund sent. The transaction ID is in refund.txid.
- ON_HOLD
- Held for manual review (funds mid-route, screening, or an interrupted action).
- EXPIRED
- No deposit arrived in time. Late deposits are refunded.
- FAILED
- Stopped with nothing to refund (no funds reached Tacet).
Errors
Errors return {"error": CODE, "message": "…", "requestId": "…", "fields"?: {…}} with a matching HTTP status. Every response carries an x-request-id header.
- 400
- BAD_JSON, BAD_IDMalformed request body or order ID.
- 403
- LIVE_DISABLEDLive trading isn't enabled on this server.
- 404
- NOT_FOUNDNo order with that ID.
- 409
- RATE_MOVEDThe rate dropped more than 1% since the quote. Response includes a new quoteId and quote.
- 422
- QUOTE_REQUIRED, QUOTE_NOT_FOUND, QUOTE_EXPIRED, QUOTE_MISMATCHThe order must reference a quote that is still valid and matches it.
- 422
- BELOW_MIN, ABOVE_MAX, SAME_ASSET, INVALID_ADDRESS, INVALID_REFUND_ADDRESS, …The request can't be quoted or ordered as given. Field errors are listed in fields.
- 413
- PAYLOAD_TOO_LARGERequest bodies are limited to 16 KB.
- 429
- RATE_LIMITEDToo many requests. Wait retryAfter seconds (also in the Retry-After header).
- 500
- INTERNAL_ERRORUnexpected error. Quote the requestId if you contact support.
- 502
- VENUE_UNAVAILABLEThe liquidity venue couldn't issue a deposit address. Retry.
- 503
- PRICES_UNAVAILABLELive market prices are unreachable; real-funds quotes are refused rather than priced from fallbacks. Retry shortly.