Radox Rail · API Stellar mainnet · sandbox on testnet

The Radox Rail API

Read who holds a shipment, who signed each handover, and check that nobody changed the record. Then run your own shipment in the sandbox: every successful write is a real transaction on Stellar's test network. No account, no key.

Base URL

https://rio-miami-rail.vercel.app · JSON · CORS open · no key, no account.

Network

?network=testnet is the default. mainnet reads the mainnet registry CDV7YI7K…. The sandbox runs on testnet only.

Limits

Per IP address, per 10 minutes: 5 new sandbox shipments, 30 sandbox writes, 20 history calls. About 20 new shipments an hour for everyone.

Production access

Want to run your own handovers on the rail? Request access.

Try the APIStellar testnet

Request

Press “Open a shipment”.

Response

—
Each call that writes returns its transaction hash and a link to the public explorer.

Get started

  1. Open a shipment

    The sandbox creates five test companies on a route (warehouse, trucker, ship, customs, buyer) and opens the shipment on the registry: two confirmed transactions, about 10 to 15 seconds. The key is shipment.key in the answer. Needs curl and jq.

    KEY=$(curl -s -X POST https://rio-miami-rail.vercel.app/api/sandbox/shipments | jq -r .shipment.key)

    Then hand it to the next company. Both companies sign:

    curl -X POST https://rio-miami-rail.vercel.app/api/sandbox/shipments/$KEY/handover
  2. See the transaction on the explorer

    Every write answers with tx.hash and tx.explorer, a stellar.expert link. There you see the contract call, its arguments and the two signatures. Nobody needs our server to read it.

  3. Verify the head

    The head is a fingerprint of the whole history: each step hashes the one before it. The history endpoint recomputes it from each transaction's own arguments and compares it with the head stored in the contract.

    curl https://rio-miami-rail.vercel.app/api/shipments/$KEY/history

    To trust no server, not even this one, press Verify the record above: your browser fetches each transaction from Stellar's public Horizon, rebuilds the head with the formula below and compares it with the head the contract returns. Any tool that can read Horizon can do the same.

Reference

Examples are real answers from Stellar testnet; long addresses and hashes are shortened here with “…”.

GET/api/shipments/{key}

The shipment's current state, read live from the contract. Works for demo and sandbox shipments. No limit of its own.

Parameters

NameInTypeRequiredFormat
keypathstringyes64 lowercase hex characters: sha256 of "<container>|<origin>"
networkquerystringnotestnet (default) or mainnet

Request

curl https://rio-miami-rail.vercel.app/api/shipments/31b4cff785831659b4ecb8fd3e7382f9760a12ef45ef7eb30f45b3b6911a5e37

Response 200

{
  "network": "testnet",
  "source": "live",
  "shipment": {
    "key": "31b4cff7…911a5e37",
    "registry": "CCBJVXMJ…R2H4LII",
    "registry_name": "demo",
    "status": "Delivered",
    "current_custodian": "GAG5WGRM…JDA35P7",
    "route": ["GAJTXDXN…UDF3K2", "GCRBNBSU…VUXSH4D", "GBSRCRAW…P2BMFE", "GA6JTLEG…AOKAOTIEW", "GAG5WGRM…JDA35P7"],
    "seq": 4,
    "head": "2d74fec0…83195414",
    "holder": "GBPFJMZ7…RLIIGDJ",
    "token": "CBCWTIT5…Z6N5Q22Z"
  },
  "contract": "https://stellar.expert/explorer/testnet/contract/CCBJVXMJ…R2H4LII"
}

Fields

sourcelive: read from the contract now. snapshot, not live: testnet could not be reached and a saved copy of a demo shipment is shown (only key, registry, head).
registry, registry_nameThe custody_registry contract that holds the shipment: demo, three-machine or sandbox.
statusOpen, Delivered or Frozen (by the registry admin).
current_custodianStellar address of the company holding the goods now.
routeEvery company on the route, in order; the first one holds the goods at opening.
seqHandovers done so far.
headFingerprint of the whole history: each step hashes the one before it (formula below).
holderWho received the goods at delivery; null before.
tokenThe asset contract the shipment names. Sandbox shipments name the native XLM contract as a placeholder.

Errors: E_KEY 400, E_NOT_FOUND 404, E_NETWORK 502, E_MAINNET_NOT_DEPLOYED 503.

GET/api/shipments/{key}/history

Every registry call for the shipment, with its transaction, and the head recomputed from each call's own arguments. Limit: 20 per 10 minutes per IP address; an answer is reused for 20 seconds.

Parameters

Same as above: key (path, required), network (query, optional).

Request

curl https://rio-miami-rail.vercel.app/api/shipments/$KEY/history

Response 200

{
  "network": "testnet",
  "shipment": { "key": "a9d2ab53…b6e1ffde", "status": "Open", "seq": 1, "head": "64cb0015…4197d12d", … },
  "steps": [
    { "step": "open", "hash": "9bac17f1…ae94bd06", "explorer": "https://stellar.expert/…", "ledger": 5393621,
      "time": "2026-10-26T05:14:52Z", "route": [ … ], "docs_root": "8e135789…", "head_after": "…" },
    { "step": "handover", "hash": "b6c23011…44755d1d", "from": "GCN7FALU…RN4GTO", "to": "GDAZRF2X…LU4LMKF",
      "seq": 1, "evidence_hash": "25f7b717…d6b998a5", "assurance": 3, "head_after": "64cb0015…4197d12d", … }
  ],
  "recomputed_head": "64cb0015…4197d12d",
  "head_matches": true
}

Fields

stepsIn ledger order: open, handover, attest, deliver, each with its transaction hash, explorer link, ledger, time and the head_after it.
assuranceDeclared by the caller: 3 = both signed in person, 2 = one side by delegation, 1 = external attestation only.
head_matchestrue when the recomputed head equals the head in the contract. null when the shipment opened more than about 7 days ago: the testnet RPC keeps contract events that long.

Errors: E_KEY 400, E_NOT_FOUND 404, E_RATE_LIMIT 429, E_NETWORK 502, E_MAINNET_NOT_DEPLOYED 503.

POST/api/sandbox/shipments

Opens a new test shipment on a five-stop route (warehouse, trucker, ship, customs, buyer). The server creates six throwaway testnet accounts (the five companies and the holder) and calls open: two confirmed transactions, about 10 to 15 seconds. No body. Limit: 5 per 10 minutes per IP address, about 20 an hour for everyone.

Request

curl -X POST https://rio-miami-rail.vercel.app/api/sandbox/shipments

Response 201

{
  "network": "testnet",
  "call": "open",
  "tx": { "hash": "9bac17f1…ae94bd06", "explorer": "https://stellar.expert/explorer/testnet/tx/9bac17f1…", "signers": ["GBD6DWV7…GKLXMDSJ"] },
  "setup_tx": { "hash": "fe5b5521…26706ce8", "explorer": "https://stellar.expert/…" },
  "container": "SBX1381138|BRRIO",
  "roles": { "warehouse": "GCN7FALU…RN4GTO", "trucker": "GDAZRF2X…LU4LMKF", "ship": "…", "customs": "…", "buyer": "…", "holder": "…" },
  "shipment": { "key": "a9d2ab53…b6e1ffde", "registry_name": "sandbox", "status": "Open", "seq": 0, … },
  "next": "POST /api/sandbox/shipments/a9d2ab53…/handover"
}

Fields

txThe open call on the registry, signed by the sandbox operator.
setup_txThe transaction that created the six test accounts.
containerA made-up container number and origin; shipment.key is its sha256.
rolesThe test address of each company and of the holder.

Errors: E_RATE_LIMIT 429, E_SANDBOX_CAP 429, E_NETWORK 502, E_TX_FAILED 502, E_SANDBOX_OFF 503, E_SANDBOX_EMPTY 503, E_MAINNET_NOT_DEPLOYED 503, E_TIMEOUT 504.

POST/api/sandbox/shipments/{key}/handover

The next handover, signed by both companies: the one giving the goods sends the transaction, the one receiving them signs its authorisation. About 5 seconds. Limit: 30 sandbox writes per 10 minutes per IP address.

Parameters

NameInTypeRequiredFormat
keypathstringyesA sandbox shipment key (64 hex).
frombodystringnowarehouse, trucker, ship, customs or buyer. Default: the current custodian. Another company is sent anyway, so the contract's refusal shows.
assurancebodyintegerno1, 2 or 3; default 3. Outside 1 to 3 the contract refuses it.
evidencebodystringnoAny text; its sha256 is stored as the handover's evidence hash.
networkquerystringnoAs above.

Request

curl -X POST https://rio-miami-rail.vercel.app/api/sandbox/shipments/$KEY/handover \
  -H 'Content-Type: application/json' -d '{"evidence":"gate-in photo 0042"}'

Response 200

{
  "network": "testnet",
  "call": "handover",
  "tx": { "hash": "b6c23011…44755d1d", "explorer": "https://stellar.expert/…", "signers": ["GCN7FALU…RN4GTO", "GDAZRF2X…LU4LMKF"] },
  "from": "warehouse",
  "to": "trucker",
  "signed_by": ["warehouse", "trucker"],
  "evidence_hash": "25f7b717…d6b998a5",
  "shipment": { "status": "Open", "current_custodian": "GDAZRF2X…LU4LMKF", "seq": 1, "head": "64cb0015…4197d12d", … }
}

Refusal 409

curl -X POST https://rio-miami-rail.vercel.app/api/sandbox/shipments/$KEY/handover \
  -H 'Content-Type: application/json' -d '{"from":"customs"}'

{
  "network": "testnet",
  "error": {
    "code": "E_WRONG_CUSTODIAN",
    "message": "The company handing over is not the one holding the shipment.",
    "refused_by": "custody_registry",
    "contract_error": 1
  }
}

Errors: E_KEY 400, E_ROLE 400, E_ASSURANCE 400, E_AUTH 403, E_NOT_FOUND 404, E_NOT_SANDBOX 404, E_WRONG_CUSTODIAN 409, E_NOT_OPEN 409, E_FROZEN 409, E_PAUSED 409, E_BAD_ASSURANCE 409, E_ROUTE_COMPLETE 409, E_RATE_LIMIT 429, E_NETWORK 502, E_TX_FAILED 502, E_SANDBOX_OFF 503, E_MAINNET_NOT_DEPLOYED 503, E_TIMEOUT 504.

POST/api/sandbox/shipments/{key}/deliver

The buyer, last on the route, hands the goods to the holder; both sign and the shipment becomes Delivered. Before every handover is done the contract refuses it. Sandbox shipments carry no token, so nothing is burned. Parameters: key (path), network (query). Same limit as handover.

curl -X POST https://rio-miami-rail.vercel.app/api/sandbox/shipments/$KEY/deliver

The answer has the same shape as a handover: call: "deliver", tx, signed_by: ["buyer", "holder"] and the shipment with status: "Delivered" and its holder.

Errors: E_KEY 400, E_AUTH 403, E_NOT_FOUND 404, E_NOT_SANDBOX 404, E_NOT_FINAL 409, E_NOT_OPEN 409, E_FROZEN 409, E_RATE_LIMIT 429, E_NETWORK 502, E_TX_FAILED 502, E_SANDBOX_OFF 503, E_MAINNET_NOT_DEPLOYED 503, E_TIMEOUT 504.

The head formula

The same bytes as the custody_registry contract. addr(x) is the XDR of the address as an ScVal; numbers are 4-byte big-endian.

open:     sha256(0x01 || key || docs_root)
handover: sha256(0x02 || head || addr(from) || addr(to) || evidence_hash || be32(seq) || be32(assurance))
attest:   sha256(0x03 || head || addr(source) || be32(kind) || event_hash || be32(seq))

Errors

Every error answers { "error": { "code", "message" } }. When the contract itself refused the call, the answer adds "refused_by": "custody_registry" and its error number, and no transaction is sent.

CodeHTTPMeaning

What this is, and is not

This is the open API of Radox Rail: it reads and writes the open-source custody_registry contract on Stellar's test network. It serves only that contract; Radox's own platform is a separate system and is not exposed here. The companies are test accounts, the keys are test keys, and no real goods or money move. Sandbox shipments go to their own registry (CBTHLS…6ERX), deployed from the same code as the demo registry (wasm 8230ed93…1342); they name the native XLM contract as a placeholder token, and no token is issued or burned.