Quick start
This guide takes you from nothing to a purchased label. You need a Ship Jay account with money in the wallet and at least one saved ship-from address.
-
Create an API key
In the app, open Settings, then Developers, and create a key. For this guide tick
shipments:read,shipments:writeandshipments:buy. Give it a per label limit such as 20.00 while you test. The key looks likesj_...and is shown once, so copy it now.Keep it out of source control and out of browsers. The API sends no CORS headers on purpose: call it from a server.
Terminal window export SHIPJAY_KEY="sj_..."// Node 18 or newer. Run with: SHIPJAY_KEY=sj_... node quickstart.mjsconst BASE = "https://shipjay.com/api/v1";async function qp(path, { method = "GET", body, headers = {} } = {}) {const res = await fetch(BASE + path, {method,headers: {Authorization: `Bearer ${process.env.SHIPJAY_KEY}`,"Content-Type": "application/json",...headers,},body: body ? JSON.stringify(body) : undefined,});if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);return res;}# pip install requests. Run with: SHIPJAY_KEY=sj_... python quickstart.pyimport osimport requestsBASE = "https://shipjay.com/api/v1"session = requests.Session()session.headers["Authorization"] = f"Bearer {os.environ['SHIPJAY_KEY']}"def qp(method, path, **kwargs):res = session.request(method, BASE + path, timeout=30, **kwargs)if not res.ok:raise RuntimeError(f"{res.status_code} {res.text}")return res -
Check the key
GET /accountworks with any valid key. It shows the organization, its currency and what the key may do.Terminal window curl https://shipjay.com/api/v1/account \-H "Authorization: Bearer $SHIPJAY_KEY"console.log(await (await qp("/account")).json());print(qp("GET", "/account").json()){"organization": { "id": "5c2d0f4e-8a31-4b6e-9d57-1f0a2c9b7e44", "name": "Northfield Candle Co", "currency": "USD" },"key": { "id": "e19b7a62-3c05-4d8f-a6b1-90d4f27c5a13", "name": "Quick start", "scopes": ["shipments:read", "shipments:write", "shipments:buy"] }} -
Find your ship-from address
A quote needs the id of one of your saved ship-from addresses.
Terminal window curl https://shipjay.com/api/v1/ship-from \-H "Authorization: Bearer $SHIPJAY_KEY"const { data: addresses } = await (await qp("/ship-from")).json();const from = addresses.find((a) => a.is_default) ?? addresses[0];addresses = qp("GET", "/ship-from").json()["data"]origin = next((a for a in addresses if a["is_default"]), addresses[0]){"data": [{"id": "0b8e6d1a-47c2-4f93-b5a8-6d2e91f3c0a7","nickname": "Workshop","name": "Northfield Candle Co","company": "Northfield Candle Co","street1": "410 Mill Street","street2": null,"city": "Northfield","state": "MN","zip": "55057","country": "US","phone": "5075550142","is_default": true,"use_as_return": true}],"next_cursor": null} -
Get rates
Quoting does not spend money. It saves the shipment, rates every enabled carrier and applies your shipping rules. The request is limited to 30 per minute per key.
Terminal window curl https://shipjay.com/api/v1/shipments/quote \-H "Authorization: Bearer $SHIPJAY_KEY" \-H "Content-Type: application/json" \-d '{"from_address_id": "0b8e6d1a-47c2-4f93-b5a8-6d2e91f3c0a7","to": {"name": "Avery Quinn","street1": "88 Harbor Lane","city": "Portland","state": "ME","zip": "04101","country": "US"},"parcel": { "packaging_type": "box", "length_in": 10, "width_in": 8, "height_in": 4, "weight_oz": 24 },"options": { "reference1": "order-1001" }}'const quote = await (await qp("/shipments/quote", {method: "POST",body: {from_address_id: from.id,to: {name: "Avery Quinn",street1: "88 Harbor Lane",city: "Portland",state: "ME",zip: "04101",country: "US",},parcel: { packaging_type: "box", length_in: 10, width_in: 8, height_in: 4, weight_oz: 24 },options: { reference1: "order-1001" },},})).json();quote = qp("POST", "/shipments/quote", json={"from_address_id": origin["id"],"to": {"name": "Avery Quinn","street1": "88 Harbor Lane","city": "Portland","state": "ME","zip": "04101","country": "US",},"parcel": {"packaging_type": "box", "length_in": 10, "width_in": 8, "height_in": 4, "weight_oz": 24},"options": {"reference1": "order-1001"},}).json()The response is
201. The rate ids are only valid for this shipment, and only untilexpires_in_secondsruns out.{"status": "quoted","shipment_id": "2f6c3a9e-5d1b-4c7e-9a41-8b0d6e7f1c20","zone": 5,"rates": [{ "id": "r_5f3a91", "carrier": "USPS", "service": "Ground Advantage", "currency": "USD", "price_cents": 864, "retail_cents": 1130, "delivery_days": 4, "delivery_date": null, "guaranteed": false, "badges": ["cheapest"] },{ "id": "r_c8d204", "carrier": "UPS", "service": "Ground", "currency": "USD", "price_cents": 1212, "retail_cents": 1590, "delivery_days": 3, "delivery_date": null, "guaranteed": false, "badges": ["best"] }],"suggested_rate_id": null,"suggested_by_rule": null,"applied_rules": [],"partial": false,"expires_in_seconds": 600}If
partialistrue, some carriers did not answer and the list can be shorter than usual. If the response has"status": "needs_address_confirmation"instead, the carrier proposed a corrected address; see the API reference for that shape. -
Buy the label
Buying needs an
Idempotency-Keyheader of 8 to 200 characters. Generate one per purchase and keep it with your order. If a request times out, send it again with the same key: you get the first result back and are never charged twice.Terminal window curl https://shipjay.com/api/v1/shipments/2f6c3a9e-5d1b-4c7e-9a41-8b0d6e7f1c20/buy \-H "Authorization: Bearer $SHIPJAY_KEY" \-H "Content-Type: application/json" \-H "Idempotency-Key: order-1001-label-1" \-d '{ "rate_id": "r_5f3a91" }'import { randomUUID } from "node:crypto";const bought = await (await qp(`/shipments/${quote.shipment_id}/buy`, {method: "POST",headers: { "Idempotency-Key": `order-1001-${randomUUID()}` },body: { rate_id: quote.rates[0].id },})).json();import uuidbought = qp("POST",f"/shipments/{quote['shipment_id']}/buy",headers={"Idempotency-Key": f"order-1001-{uuid.uuid4()}"},json={"rate_id": quote["rates"][0]["id"]},).json(){"shipment_id": "2f6c3a9e-5d1b-4c7e-9a41-8b0d6e7f1c20","status": "purchased","tracking_code": "9400100000000000000000","balance_after_cents": 18636,"duplicate": false}A
statusofpurchasingmeans the purchase is still finishing. Repeat the same request later, or read the shipment, until it sayspurchased. -
Download the label
The label is returned as a file in the format chosen for the shipment (PDF by default).
Terminal window curl https://shipjay.com/api/v1/shipments/2f6c3a9e-5d1b-4c7e-9a41-8b0d6e7f1c20/label \-H "Authorization: Bearer $SHIPJAY_KEY" \-o label.pdfimport { writeFile } from "node:fs/promises";const label = await qp(`/shipments/${bought.shipment_id}/label`);await writeFile("label.pdf", Buffer.from(await label.arrayBuffer()));label = qp("GET", f"/shipments/{bought['shipment_id']}/label")with open("label.pdf", "wb") as f:f.write(label.content) -
Read tracking
Terminal window curl https://shipjay.com/api/v1/shipments/2f6c3a9e-5d1b-4c7e-9a41-8b0d6e7f1c20/tracking \-H "Authorization: Bearer $SHIPJAY_KEY"const tracking = await (await qp(`/shipments/${bought.shipment_id}/tracking`)).json();console.log(tracking.status, tracking.tracking_url);tracking = qp("GET", f"/shipments/{bought['shipment_id']}/tracking").json()print(tracking["status"], tracking["tracking_url"]){"tracking_code": "9400100000000000000000","status": "pre_transit","tracking_url": "https://shipjay.com/t/k3v9...","events": []}
Next steps
Section titled “Next steps”- Move from polling to webhooks.
- Read Buying labels safely before you put the buy call in production code.
- Let an assistant do this for you with the MCP server.
© 2026 Ship Jay. All rights reserved. Created & Developed by WCWS Internet
