# 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.

**Buying spends real money:** Buying a label takes money from your wallet. Use a key with a low per label limit while you test, and refund labels you do not need (see [Buying labels safely](/docs/buying-labels)).

1. **Create an API key**

   In the app, open Settings, then Developers, and create a key. For this guide tick `shipments:read`, `shipments:write` and `shipments:buy`. Give it a per label limit such as 20.00 while you test. The key looks like `sj_...` 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.

   ```bash
   export SHIPJAY_KEY="sj_..."
   ```
   ```js
   // Node 18 or newer. Run with: SHIPJAY_KEY=sj_... node quickstart.mjs
   const 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;
   }
   ```
   ```python
   # pip install requests. Run with: SHIPJAY_KEY=sj_... python quickstart.py
   import os
   import requests

   BASE = "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
   ```
2. **Check the key**

   `GET /account` works with any valid key. It shows the organization, its currency and what the key may do.

   ```bash
   curl https://shipjay.com/api/v1/account \
     -H "Authorization: Bearer $SHIPJAY_KEY"
   ```
   ```js
   console.log(await (await qp("/account")).json());
   ```
   ```python
   print(qp("GET", "/account").json())
   ```
   ```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"] }
   }
   ```

3. **Find your ship-from address**

   A quote needs the id of one of your saved ship-from addresses.

   ```bash
   curl https://shipjay.com/api/v1/ship-from \
     -H "Authorization: Bearer $SHIPJAY_KEY"
   ```
   ```js
   const { data: addresses } = await (await qp("/ship-from")).json();
   const from = addresses.find((a) => a.is_default) ?? addresses[0];
   ```
   ```python
   addresses = qp("GET", "/ship-from").json()["data"]
   origin = next((a for a in addresses if a["is_default"]), addresses[0])
   ```
   ```json
   {
     "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
   }
   ```

4. **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.

   ```bash
   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" }
     }'
   ```
   ```js
   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();
   ```
   ```python
   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 until `expires_in_seconds` runs out.

   ```json
   {
     "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 `partial` is `true`, 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.

5. **Buy the label**

   Buying needs an `Idempotency-Key` header 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.

   ```bash
   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" }'
   ```
   ```js
   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();
   ```
   ```python
   import uuid

   bought = qp(
       "POST",
       f"/shipments/{quote['shipment_id']}/buy",
       headers={"Idempotency-Key": f"order-1001-{uuid.uuid4()}"},
       json={"rate_id": quote["rates"][0]["id"]},
   ).json()
   ```
   ```json
   {
     "shipment_id": "2f6c3a9e-5d1b-4c7e-9a41-8b0d6e7f1c20",
     "status": "purchased",
     "tracking_code": "9400100000000000000000",
     "balance_after_cents": 18636,
     "duplicate": false
   }
   ```

   A `status` of `purchasing` means the purchase is still finishing. Repeat the same request later, or read the shipment, until it says `purchased`.

6. **Download the label**

   The label is returned as a file in the format chosen for the shipment (PDF by default).

   ```bash
   curl https://shipjay.com/api/v1/shipments/2f6c3a9e-5d1b-4c7e-9a41-8b0d6e7f1c20/label \
     -H "Authorization: Bearer $SHIPJAY_KEY" \
     -o label.pdf
   ```
   ```js
   import { writeFile } from "node:fs/promises";

   const label = await qp(`/shipments/${bought.shipment_id}/label`);
   await writeFile("label.pdf", Buffer.from(await label.arrayBuffer()));
   ```
   ```python
   label = qp("GET", f"/shipments/{bought['shipment_id']}/label")
   with open("label.pdf", "wb") as f:
       f.write(label.content)
   ```
7. **Read tracking**

   ```bash
   curl https://shipjay.com/api/v1/shipments/2f6c3a9e-5d1b-4c7e-9a41-8b0d6e7f1c20/tracking \
     -H "Authorization: Bearer $SHIPJAY_KEY"
   ```
   ```js
   const tracking = await (await qp(`/shipments/${bought.shipment_id}/tracking`)).json();
   console.log(tracking.status, tracking.tracking_url);
   ```
   ```python
   tracking = qp("GET", f"/shipments/{bought['shipment_id']}/tracking").json()
   print(tracking["status"], tracking["tracking_url"])
   ```
   ```json
   {
     "tracking_code": "9400100000000000000000",
     "status": "pre_transit",
     "tracking_url": "https://shipjay.com/t/k3v9...",
     "events": []
   }
   ```

## Next steps

- Move from polling to [webhooks](/docs/webhooks).
- Read [Buying labels safely](/docs/buying-labels) before you put the buy call in production code.
- Let an assistant do this for you with the [MCP server](/docs/mcp).