Placing and cancelling orders
The three steps that place an order, the three that cancel one, how to confirm each, and the rules that catch people out.
Placing an order takes three steps: prepare with Cardiv, sign locally, submit to Calden. Cancelling takes the same three. A fourth, reading the order back, confirms either.
The examples use curl and Node, with jq to trim output, and these set once per shell. PARTY is your party ID, from the chip in the app header or from GET /account. PRIV is the private key from Calden (Settings, Backup Keys, Reveal private key, then Copy), which comes out as base64.
export BASE="https://api.beta.cardiv.xyz/api/trade/v1"
export KEY="usr_ak_..."
export PARTY="Calden::1220..."
export PRIV="<your wallet private key, base64>"Prepare
POST /orders, measured at 9 to 14 seconds.
| Field | Required | |
|---|---|---|
pair | yes | From GET /pairs, verbatim |
side | yes | BUY commits the quote token, SELL commits the base |
type | yes | LIMIT, MARKET or STOP_LOSS |
price | for LIMIT | Quote per unit of base, decimal string, at most 10 decimal places and on the pair's priceTick. Longer values are rejected, not rounded. |
stopPrice | for STOP_LOSS | The trigger price. Stop-loss orders are SELL only. |
quantity | yes | Base units, decimal string. price times quantity must meet the pair's minQuoteAmount. |
clientOrderId | recommended | Must start with your account's clientOrderIdPrefix, api- for a retail account or the slug plus a hyphen for a market maker, at most 120 characters. Generated when omitted. It is the idempotency key. |
The example quotes CBTC/CC. Take the price from the book rather than typing one: a sell at the best ask joins that level and rests without filling, which leaves you an order to cancel further down. On an environment that trades other pairs, pick one from GET /pairs and a quantity above its minQuoteAmount.
ASK=$(curl -s "$BASE/orderbook/CBTC%2FCC?depth=1" | jq -r '.data.asks[0][0]')
curl -s -X POST "$BASE/orders" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"pair":"CBTC/CC","side":"SELL","type":"LIMIT",
"price":"'$ASK'","quantity":"0.0001","clientOrderId":"api-first-order"}' \
| tee prepare.jsonA 202 comes back with nothing on the book and nothing locked. An abandoned prepare expires with no funds committed.
| Response field | |
|---|---|
order.orderId | Your clientOrderId |
order.providerFeeAmount, order.providerFeeSymbol | The fee baked into the transaction, in the asset the order locks. Always "0" for a registered market maker. Check it before you sign. |
order.status, order.contractId | null until the transaction commits |
signing.preparedTransactionHash | The value to sign |
signing.hashingSchemeVersion, signing.commandId, signing.submitToken | Echoed verbatim in the submission body |
signing.submitUrl | Absolute URL on the Calden host |
Always set your own clientOrderId. If your client never sees the response, that id is the only way to find out whether the order exists. Preparing again with the same id cannot create a second order: when the first is already live, the response returns it with no signing block.
Only one prepared order can await your signature at a time. A second prepare before the first is signed or has expired returns LEDGER_COMMAND_REJECTED.
Sign
Ed25519 over the raw bytes of the base64-decoded preparedTransactionHash. No re-hashing, no prefix, and not the base64 string itself. Calden verifies the signature against the exact bytes it cached.
Submit
POST to signing.submitUrl, measured at 18 to 20 seconds, with no authentication header:
{
"partyId": "<your party>",
"commandId": "<signing.commandId>",
"signature": "<base64>",
"publicKey": "<base64, the key registered for the party>",
"submitToken": "<signing.submitToken>",
"prepared": {
"preparedTransactionHash": "<signing.preparedTransactionHash>",
"hashingSchemeVersion": "<signing.hashingSchemeVersion>"
}
}The script below signs and submits from a saved prepare response. It needs Node 20 or later and npm install tweetnacl. Both the 32-byte seed and the 64-byte secret key forms of the private key work.
// Usage: PARTY=... PRIV=... node sign-and-submit.js prepare.json
const fs = require('fs');
const nacl = require('tweetnacl');
const raw = new Uint8Array(Buffer.from(process.env.PRIV, 'base64'));
const secretKey = raw.length === 64 ? raw : nacl.sign.keyPair.fromSeed(raw).secretKey;
const publicKey = Buffer.from(nacl.sign.keyPair.fromSecretKey(secretKey).publicKey).toString('base64');
const s = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')).data.signing;
if (!s || !s.preparedTransactionHash) { console.log('Nothing to sign'); process.exit(0); }
// Sign the decoded hash bytes. No re-hashing, no prefix.
const hash = new Uint8Array(Buffer.from(s.preparedTransactionHash, 'base64'));
const body = {
partyId: process.env.PARTY,
commandId: s.commandId,
submitToken: s.submitToken,
prepared: { preparedTransactionHash: s.preparedTransactionHash, hashingSchemeVersion: s.hashingSchemeVersion },
signature: Buffer.from(nacl.sign.detached(hash, secretKey)).toString('base64'),
publicKey,
};
// A short timeout abandons transactions that go on to commit. Give it three minutes.
fetch(s.submitUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(180000),
}).then(async (r) => { console.log(r.status, await r.text()); process.exit(r.ok ? 0 : 1); });node sign-and-submit.js prepare.jsonA 200 means accepted and dispatched, not resting. Confirmation is asynchronous, and a 200 has been seen for a transaction that never confirmed while the network was degraded. Set your client timeout on the submission to at least 120 seconds: submissions queue behind a traffic lock and retry consensus timeouts internally, so a short timeout abandons transactions that go on to commit.
If a submission fails, times out, or your client dies before it reads the response, the order may still have committed. Read it back before you retry anything.
Confirm
GET /orders/{orderId} answers for one order without listing the rest, and never triggers a wallet refresh. Measured end to end: 32 seconds from submission to a confirmed OPEN, usually much less.
curl -s "$BASE/orders/api-first-order" -H "X-API-Key: $KEY" \
| jq '.data.order | {orderId, contractId, status, price, quantity, filled}'Until it reads OPEN the order is not on the book. Once it does, it is in the public book at your price, which anyone can see, and it counts among your resting orders:
curl -s "$BASE/orderbook/CBTC%2FCC?depth=50" | jq --arg p "$ASK" '.data.asks[] | select(.[0] == $p)'
curl -s "$BASE/orders?refreshCalden=false" -H "X-API-Key: $KEY" | jq '.data.orders | length'Update your own book state only once you see OPEN.
Deadlines
| What | How long | On expiry |
|---|---|---|
| Prepared transaction | 5 minutes | A later submission returns 410 CACHE_EXPIRED; prepare again with the same clientOrderId |
submitToken | 20 minutes | Not the limiting factor |
Sign and submit immediately after preparing. A delayed submission spends the five minute window while the price you set goes stale.
Cancelling
POST /orders/{contractId}/cancel, prepare measured at 11 to 23 seconds, then the same sign and submit steps. The body {"reason": "..."} is optional. The response carries cancelFee, with charged, amount and symbol, and the same signing block; a registered market maker sees charged: false.
curl -s -X POST "$BASE/orders/api-first-order/cancel" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"reason":"manual"}' | tee prepare.json
node sign-and-submit.js prepare.jsonThe path takes the current contractId or the orderId. Cancel by orderId: the contractId changes after a partial fill, and the previous one returns 404 CONTRACT_NOT_FOUND, while the orderId never changes. Wait until the order reads OPEN first; a cancel prepared while the placement is still settling is refused as not found and must be tried again.
Then confirm GET /orders/{orderId} reads CANCELLED. Cancel one order per call; there is no bulk endpoint. Signed submissions may be issued concurrently, subject to the capacity limit in Timings and limits.
Never withdraw from your wallet the funds backing a resting order. Doing so strands the order for good: neither your cancellation nor a Cardiv-side cancellation can complete, and clearing it takes manual work by Cardiv.
What happens to your funds
Exactly as in the app. A buy commits the quote token and a sell commits the base, locked in your own wallet from the moment the placement commits until the order fills or you cancel it. A fill moves both legs in one transaction, and a cancellation releases the lock in the same transaction as the cancellation. The fee, when one applies, leaves your wallet in that same transaction.
Need help
Write to support@wolfedgelabs.com and include your party ID.