Trading OS API Documentation

Public developer documentation for the Trading OS API. Connect external applications, ERPs, and custom scripts to Trade Node.

Two directions. One contract each.

Trade Node syncs both ways, and each way has its own published contract. Read the one that matches what you are building.

  • Your system calls Trade Node. The Trading OS API: a key the business issues, an explicit list of operations, an idempotency key on every write. Start at Authentication.
  • Trade Node calls your system. The partner adapter contract: one HTTPS endpoint you publish, four operations, an HMAC signature on every request. Start at The second direction.

Both run against the same records and the same rules. Nothing in either direction bypasses the validations, permissions or module switches that apply to a person using the app.

Per-business API keys

Every business authenticates with its OWN credentials. Trade Node holds no shared vendor account.

Keys are created by the business inside their Trade Node portal, scoped to an explicit list of operations (default deny), and run with the key owner's permissions. Pass the token in the standard HTTP Authorization header. Base URL: https://trade-erp-gggnx.replit.app/api/trading-os/v1.
curl -X GET https://trade-erp-gggnx.replit.app/api/trading-os/v1/catalog \
  -H "Authorization: Bearer tn_os_..."

The operation envelope

A single POST endpoint multiplexes all writes and deep reads.

Send a POST to /operations/{operationId} with a JSON envelope containing path, query and body. The operation id is the one from the catalog; the envelope carries what a browser would have put in the URL and the form.
curl -X POST https://trade-erp-gggnx.replit.app/api/trading-os/v1/operations/products.create \
  -H "Authorization: Bearer tn_os_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: req_12345" \
  -d '{
    "body": {
      "name": "Steel Pipe 20mm",
      "category": "Pipes",
      "unit": "KG"
    }
  }'

Idempotency by default

Never create a duplicate invoice by accident.

Every write REQUIRES an Idempotency-Key header. Replaying the same key returns the first outcome rather than doing the work twice. Reusing a key with a different request is refused outright, so a key cannot silently address the wrong record. A write interrupted before its outcome was known is reported as uncertain and is never retried automatically — a person decides.

When Trade Node calls you

For systems that cannot call us — or should not have to — Trade Node does the calling. You publish one endpoint; the business configures the rest.

A business adds a connection in their Trade Node account under Other software (custom adapter), enters your endpoint and a signing secret, and then says what should travel: which Trade Node records, in which direction, and which fields line up with which of yours.

  • Mappings pair one Trade Node read with one of your operations, in a direction of pull, push or both.
  • Links remember which of your ids belongs to which Trade Node record. This is what makes the second write an edit instead of a second copy.
  • Conflicts are raised, not guessed. When both sides changed the same record since the last sync, the record is held and a person picks the winner.
  • Uncertain writes — a call that failed after your system may already have applied it — hold the record instead of repeating the write.

You implement four operations. Everything above is Trade Node's side of the job.

One endpoint, four operations

Every call is a POST to the same URL. The operation says what kind of call it is; the resource says what it is about.

  • masters.read — return standing records: products, product categories, suppliers, ledger accounts.
  • masters.write — create or update one standing record. When externalId is present, it already exists on your side.
  • documents.read — return transactions: orders, invoices, dispatches, payments.
  • documents.write — create or update one transaction.

Four operations, not four collections. masters.write carries a product, a supplier or a ledger account, and the resource field is what says which — "products", "suppliers", "ledger". Switch on it. The full list of values is published in the contract JSON under request.resources, and it travels inside the signed body like everything else.

mapping is the business's own mapping behind the call, and it is stable. A business can sync the same kind of record to two different places, each keeping its own ids, so file your ids against the pair — (mapping, externalId) — rather than externalId alone. It is null only when the call belongs to no mapping: a connection test, or a check run before any mapping is set up.

The endpoint must be a public HTTPS address. Plain HTTP, a private or loopback address, and a hostname that resolves to one are all refused before any request is made — a local adapter is reached through a tunnel with a public name. Redirects are refused too, so publish the final URL.

POST https://your-platform.example.com/trade-node
x-tradenode-timestamp: 1789200000000
x-tradenode-nonce: 0f5c2b6e-1f2a-4f0e-9a5f-9b3f0f2a7c11
x-tradenode-signature: 9c1f...64 hex chars
content-type: application/json

{
  "operation": "masters.write",
  "resource": "products",
  "mapping": 12,
  "params": {},
  "body": { "name": "Steel Pipe 20mm", "unit": "KG" },
  "externalId": "CUST-4471",
  "idempotencyKey": "push:12:upd:4471:w3:c47b9e",
  "cursor": null,
  "nonce": "0f5c2b6e-1f2a-4f0e-9a5f-9b3f0f2a7c11",
  "timestamp": "1789200000000"
}

Prove the call came from Trade Node

HMAC-SHA256 over the timestamp, the nonce and the raw body, keyed with the secret the business gave you.

Sign the bytes you received. Re-serialising the parsed JSON changes key order and whitespace, and the signature will not match. Compare in constant time, and reject a timestamp outside your own replay window — five minutes is the recommended figure, which covers a slow queue and a little clock drift. The nonce is fresh on every call, so you can refuse one you have already seen inside that window.

A request that fails either check should be answered 401. Trade Node runs a check that deliberately sends a bad signature and an hour-old timestamp, and reports an adapter that accepts them as a failure.

import { createHmac, timingSafeEqual } from "node:crypto";

// raw must be the exact request body, before JSON.parse
function verify(raw, headers, secret) {
  const ts = headers["x-tradenode-timestamp"];
  const nonce = headers["x-tradenode-nonce"];
  const presented = headers["x-tradenode-signature"];
  if (Math.abs(Date.now() - Number(ts)) > 5 * 60_000) return false;
  const expected = createHmac("sha256", secret)
    .update(ts + "." + nonce + "." + raw)
    .digest("hex");
  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(String(presented), "utf8");
  return a.length === b.length && timingSafeEqual(a, b);
}

What your adapter answers

Reads return rows. Writes return the id you filed the record under. The status code decides what happens next.

  • Answer 2xx with a JSON object. Anything outside the 2xx range is a failure, and a body that is not JSON fails the call even on a 200. An empty 204 reads as an empty object — fine for a read that found nothing, but it leaves a write with no id to link.
  • Reads: put the rows in a top-level records array. If your system answers in its own envelope, return it under data and the business points the mapping's external record path at the array inside it.
  • Switch on resource, not on operation. One write operation covers every standing record, so operation alone cannot tell you whether the body is a supplier or a product. Refuse a resource you do not handle with a 422 and say so in the message.
  • Writes: return externalId, the id your system now holds the record under. Trade Node stores it and addresses the same record with it next time. This is what stops a second copy being created.
  • Deduplicate on idempotencyKey, which every write carries inside the signed body. Store it against what you did, and if the same key arrives again return the first outcome — the same externalId — instead of acting twice. This is what protects a create: it has no externalId yet, so nothing else tells it apart from the next new record. Trade Node never repeats a write by itself, but a write whose outcome it never learned is put to a person, and releasing it sends the same key again. A genuinely new change always carries a new key, including an edit back to a value you held before.
  • A 429 pauses the connection, it does not fail the record. Trade Node stops that connection's cycle and waits — Retry-After is honoured, as seconds or as a date, up to an hour; without it, a minute. A read resumes from the same page, and a write answered 429 goes back into the queue exactly as it was, because nothing looked at it. Nothing else is retried inside a run: a failed read is simply read again next cycle.
  • A 5xx on a write marks the record uncertain — it may have landed with you — so it is held and shown to a person rather than written twice. A dropped connection, or an answer that is not JSON, is treated the same way.
  • A 4xx on a write is taken at its word: recorded as a refusal and reported to the business with your message, truncated to 300 characters — so put something useful in it. The record waits for someone to act; it does not queue.
  • cursor and hasMore are how Trade Node pages. Answer with a page, hasMore: true and a cursor, and it calls straight back carrying that cursor — up to 20 pages or 500 records in one cycle. Where it stopped is stored on the mapping, so the next cycle resumes there instead of asking for your first page again. hasMore: false ends the pass. Never hand back a cursor you have already given in the same run: that is a loop, and Trade Node stops the walk rather than following it.
  • params.updatedSince keeps a large catalogue cheap. Every read after the first completed pass carries an ISO 8601 UTC instant. Answer with the records created or changed at or after it and you never serialise your whole catalogue again. Ignore it and answer with everything — Trade Node fingerprints what arrives and applies only what really differs, so the result is identical, just heavier.

Check your adapter against the contract

The business can run the handshake against your endpoint from their own account, and see which part of it you keep.

The check sends one correctly signed read, then three requests you are supposed to refuse: a wrong signature, an hour-old timestamp, and an operation that does not exist. Each is reported separately, so the answer is "the signature is not being checked" rather than "sync failed". It never sends a write, so it is safe to run against a live system.

  • Endpoint is a public HTTPS address.
  • A correctly signed read is answered.
  • The answer carries records or data.
  • A wrong signature is refused with 401.
  • A stale timestamp is refused with 401.
  • An operation you do not implement is refused with 400, 404, 422 or 501.

An adapter that answers everything successfully fails three of these six, and it fails them for a reason: if you never check the signature, anyone who learns your URL can write into your system.

What it does not do: it sends one read, of one resource, and never a write. A green result says that read was answered and those three requests were refused — not that your writes work, and not that every resource is handled. It is a check on the handshake, not a certificate for the adapter.

Only the refusal the contract asks for passes those last three. A 500, a dropped connection, or an unrelated status such as a 429 rate limit is reported as inconclusive rather than credited to you: none of them shows the check is there. An adapter that is simply broken, or one that throttles the probe, should not come out of this looking secure.

Generate, do not transcribe

Both contracts are published as JSON. No key is needed to read them.

  • GET /api/trading-os/v1/catalog — every callable operation, and every system Trade Node connects to.
  • GET /api/trading-os/v1/openapi.json — OpenAPI for the API your system calls.
  • GET /api/trading-os/v1/adapter-contract.json — the adapter contract in full: operations, signing rule, response rules, conformance checks.
  • GET /api/trading-os/v1/adapter-openapi.json — OpenAPI for the endpoint YOU implement. Generate a server stub from it.

The contract carries a dated version. A new version is published alongside the old one, and the fields above are only ever added to.

curl https://trade-erp-gggnx.replit.app/api/trading-os/v1/adapter-openapi.json \
  -o trade-node-adapter.json
# then generate a server from it with your usual OpenAPI tooling

Truthful connectors, no middlemen

We do not claim "plug and play with any software". Every integration follows official boundaries.

  • Tally: connects via the customer's own desktop using a local bridge program. Outbound only; no inbound port is ever opened.
  • Marg: uses official Marg web API packages. The customer must hold the relevant Marg package: mobile-app (covers masters, stock, sales orders, dispatch status), mobile-app-billing (adds inventory vouchers/invoices), or corporate-data (covers corporate masters, balances, outstanding, invoices and accounting).
  • Zoho Books: uses the official OAuth cloud API with per-business credentials. Region-aware.
  • BUSY: has NO published public API. Integration requires a licensed BUSY partner adapter built to the contract on this page. We do not ship a native BUSY connector.
  • Anything else: the adapter contract on this page. If your platform publishes an endpoint that keeps it, a Trade Node business can connect you without either of us holding an account with the other.

Operation Catalog

A freeze of exactly what operations can be called. Grouped by module.

Products

Orders

Quotes

Invoices

Dispatches

Payments

Ledger

Stock

Purchasing

Expenses

CRM

Accounting