{ }API · REST + OPENAPI

Quote, buy, track. Over REST.

Three calls to a label. JSON in, JSON out, documented in OpenAPI, sandbox first. Any language that can make an HTTP request — and any AI agent that can read a spec.

BASE URL & AUTH

One host, one version.

BASE URL
https://api.321label.com/v1
Also reachable as https://321label.com/v1. HTTPS only.
AUTHENTICATION
Authorization: Bearer …
Every created label returns an order_token that reads, tracks and cancels that order. Account API keys are self-serve — create one under /account#/developers.
SANDBOX
"sandbox": true
A full dry-run against the carriers' test systems. Labels look real, cost nothing and never ship. Flip it to false when you go live.
THE PUBLIC ENDPOINTS

Seven routes. Three you'll use daily.

POST/v1/quotesLive rates for a shipment, cheapest first
POST/v1/labelsCreate the label · returns pdf_url, tracking, order_token
GET/v1/labels/{id}Read an order back · status, price, payment state
POST/v1/labels/{id}/cancelVoid a label · idempotent
GET/v1/shipments/{ref}Track by tracking number, order id or label id
GET/v1/carriersCarriers, services and where they're offered
POST/v1/addresses/parseBest-effort: postal addresses out of text or an image
THE THREE CALLS

A label in three requests.

Addresses and parcels share one shape across every call. Money is in major units, in the currency you ask for — the desk converts and charges in that currency.

1 · QUOTEcurl -X POST https://api.321label.com/v1/quotes \ -H "Content-Type: application/json" \ -d '{ "sandbox": true, "currency": "DKK", "from_address": { "name": "Lars Nielsen", "address_1": "Nørregade 12", "zip": "8000", "city": "Aarhus", "country": "DK" }, "to_address": { "name": "Freja Lindqvist", "address_1": "Nørrebrogade 44", "zip": "2200", "city": "København N", "country": "DK" }, "parcels": [ { "weight": 1.2, "length": 30, "width": 20, "height": 10 } ] }'
RESPONSE{ "quote_id": "q_8c1f…", "currency": "DKK", "rates": [ { "carrier": "postnord", "service": "parcelshop", "price": { "amount": 41.80, "currency": "DKK" }, "estimated_delivery": { "min_business_days": 2, "max_business_days": 3 } }, … cheapest first ] }
2 · CREATEcurl -X POST https://api.321label.com/v1/labels \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-10432" \ -d '{ "sandbox": true, "currency": "DKK", "client_id": "[email protected]", "carrier": "cheapest", "service": "parcelshop", "from_address": { … }, "to_address": { … }, "parcels": [ { "weight": 1.2, "length": 30, "width": 20, "height": 10 } ] }' → 201 { "label_id": "lbl_7f2k…", "status": "…", "carrier": "postnord", "tracking_number": "…", "price": { "amount": 41.80, "currency": "DKK" }, "pdf_url": "…", "payment_url": "…", "order_token": "otk_…" }
3 · TRACKcurl https://api.321label.com/v1/shipments/{tracking_number} \ -H "Authorization: Bearer otk_…"
CONVENTIONS

Boring on purpose.

🐍
snake_case

Every key, request and response. Dates are ISO 8601, countries ISO 3166-1 alpha-2.

💱
Money in major units

40.00 means forty — never cents. Quotes, totals, labels and receipts all in the currency you asked for: DKK, EUR, SEK, NOK, GBP or USD.

🔁
Idempotency-Key

Send one on POST /v1/labels and a retried request can't buy two labels. Cancel is idempotent by status — no key needed.

🧯
One error shape

{ "error": { "code", "message", "doc_url", "field" } } — never a stack trace, never an internal name.

ALSO FOR AGENTS

The spec is readable by machines too.

Point an agent at /llms.txt (or /llms-full.txt) and it learns the API. Prefer tools over REST? The same desk is an MCP server.

YOUR API KEYS

Building against it now? Grab a key.

The sandbox is open to try without an account. Account API keys and an optional prepaid wallet are self-serve — create yours in a minute.