Octoship
v1 · prototype

The Octoship API.

Rates from 550+ services across USPS, UPS, FedEx, DHL Express and more, plus labels, customs, pickups, returns and tracking, over plain REST and JSON.

Introduction

Getting started#

The Octoship API is organized around REST. Send JSON, get JSON back, and use standard HTTP verbs and status codes. One request quotes every courier we support, domestic or to 220+ countries and territories.

Base URLhttps://api.octoship.com/v1
  1. 01

    Get a test key

    Settings → API keys. Test keys start with os_test_ and never charge you.

  2. 02

    Quote rates

    POST a parcel and destination to /v1/rates. Get every courier, ranked.

  3. 03

    Buy the label

    POST that rate id to /v1/labels. You get a PDF, customs docs and a tracker.

This is a prototype reference. Endpoints, fields and keys on this page are illustrative and don't hit a live server.

Authenticate with a secret API key in the Authorization header as a Bearer token. Keep live keys on your server; never ship them in a browser or app bundle.

curlAuthenticated request
curl https://api.octoship.com/v1/account \
  -H "Authorization: Bearer os_test_4eC39HqLyjWDarjt"

Key types

os_test_…test mode
Free, fake labels marked VOID. Rates are real-looking, nothing is charged.
os_live_…live mode
Buys real labels from your balance. Rotate from Settings at any time.
Core resources

Address validation#

Check and standardize an address before you quote or buy. We fix typos, add missing details like ZIP+4, flag residential vs. commercial, and tell you whether it's deliverable. Labels validate automatically too.

POST/v1/addresses/validateValidate an address

Body parameters

line_1stringrequired
Street address.
line_2string
Apartment, suite, unit.
citystringrequired
City or locality.
statestring
State, province or region, where the country uses one.
postal_codestring
Postal or ZIP code.
countrystringrequired
ISO 3166-1 alpha-2, e.g. US, DE.
curlRequest
curl https://api.octoship.com/v1/addresses/validate \
  -H "Authorization: Bearer os_test_4eC39HqLyjWDarjt" \
  -H "Content-Type: application/json" \
  -d '{
    "line_1": "12 harbour wy apt 4",
    "city": "brooklin",
    "state": "NY",
    "postal_code": "11201",
    "country": "US"
  }'
jsonResponse · 200 OK
{
  "object": "address_validation",
  "status": "corrected",
  "deliverable": true,
  "residential": true,
  "address": {
    "line_1": "12 Harbor Way Apt 4",
    "city": "Brooklyn",
    "state": "NY",
    "postal_code": "11201-3307",
    "country": "US"
  },
  "changes": ["line_1", "city", "postal_code"]
}
Core resources

Rates#

A rate is a price for one courier service for one parcel between two addresses. One call returns rates from every courier that serves the route, each tagged cheapest, fastest or best_value where it applies. For international destinations, rates include estimated duties and taxes. Rates expire at the end of the day.

POST/v1/ratesGet rates for a parcel

Body parameters

originaddressrequired
Ship-from. At minimum postal_code and country.
destinationaddressrequired
Ship-to. Set residential for accurate pricing.
destination.countrystringrequired
ISO country code. Anything other than the origin country makes it international and requires customs.
parcel.weight_oznumberrequired
Weight in ounces, up to 2,400 (150 lb).
parcel.lengthnumber
Inches. With width and height, enables dim weight and USPS Cubic pricing.
parcel.widthnumber
Inches.
parcel.heightnumber
Inches.
customsobject
Required for international. See Customs & duties.
insurance.amountnumber
Declared value to insure. The premium is returned on each rate.
couriersstring[]
Limit to couriers, e.g. usps, ups, fedex, dhl. Defaults to all.
servicesstring[]
Limit to services, e.g. fedex_ground, dhl_express_worldwide.
sort_byenum
cheapest (default), fastest or best_value.
curlRequest · US → Germany
curl https://api.octoship.com/v1/rates \
  -H "Authorization: Bearer os_test_4eC39HqLyjWDarjt" \
  -H "Content-Type: application/json" \
  -d '{
    "origin": { "postal_code": "90210", "country": "US" },
    "destination": { "postal_code": "10115", "country": "DE", "residential": true },
    "parcel": { "weight_oz": 32, "length": 10, "width": 8, "height": 4 },
    "customs": {
      "incoterms": "DDP",
      "contents": "merchandise",
      "items": [
        { "description": "Ceramic mug", "hs_code": "6912.00", "quantity": 1,
          "value": 28.00, "currency": "USD", "origin_country": "US" },
        { "description": "Linen apron", "hs_code": "6211.42", "quantity": 2,
          "value": 32.00, "currency": "USD", "origin_country": "US" }
      ]
    },
    "insurance": { "amount": 92.00, "currency": "USD" },
    "couriers": ["dhl", "fedex", "ups", "usps"],
    "sort_by": "cheapest"
  }'
jsonResponse · 200 OK
{
  "object": "list",
  "shipment_id": "shp_9Lr2xQ",
  "destination_country": "DE",
  "incoterms": "DDP",
  "data": [
    {
      "id": "rate_7Dh3WxK",
      "courier": "dhl",
      "service": "dhl_express_worldwide",
      "service_name": "DHL Express Worldwide",
      "amount": 31.80,
      "retail_amount": 82.70,
      "duties_and_taxes": { "duty": 3.68, "tax": 18.13, "total": 21.81 },
      "insurance_amount": 1.01,
      "total_amount": 54.62,
      "currency": "USD",
      "delivery_days": { "min": 2, "max": 4 },
      "billed_weight_oz": 32,
      "tags": ["cheapest", "fastest"],
      "expires_at": "2026-10-28T23:59:59Z"
    },
    {
      "id": "rate_5Fx9TcE",
      "courier": "fedex",
      "service": "fedex_international_economy",
      "service_name": "FedEx International Economy",
      "amount": 33.10,
      "retail_amount": 76.15,
      "duties_and_taxes": { "duty": 3.68, "tax": 18.13, "total": 21.81 },
      "insurance_amount": 1.01,
      "total_amount": 55.92,
      "currency": "USD",
      "delivery_days": { "min": 4, "max": 6 },
      "billed_weight_oz": 32,
      "tags": ["best_value"],
      "expires_at": "2026-10-28T23:59:59Z"
    }
  ],
  "has_more": true
}

Domestic shipments are the same call, minus customs:

jsDomestic body
{
  "origin": { "postal_code": "90210", "country": "US" },
  "destination": { "postal_code": "10001", "country": "US" },
  "parcel": { "weight_oz": 12 }
}
// → usps_ground_advantage, ups_ground_saver, fedex_ground_economy, …

Couriers & services

  • uspsusps_ground_advantage, usps_priority, usps_priority_express, usps_priority_mail_international
  • upsups_ground_saver, ups_ground, ups_2nd_day_air, ups_worldwide_saver
  • fedexfedex_ground_economy, fedex_ground, fedex_2day, fedex_international_economy
  • dhldhl_express_worldwide, dhl_express_1200
  • canada_post, royal_mail, australia_post, …Postal services worldwide. 550+ services in total; list them with GET /v1/services.

Customs & duties

Send a customs object for international shipments. We generate the customs forms and commercial invoice for the label, and estimate duties and taxes per rate from your HS codes and values. With DDP you prepay duties (collect them from your buyer at checkout); with DDU the courier collects them on delivery.

customs

incotermsenumrequired
DDP (delivered duty paid) or DDU (delivered duty unpaid).
contentsenum
merchandise (default), gift, documents, sample or return.
items[].descriptionstringrequired
Plain-language description, e.g. “Ceramic mug”.
items[].hs_codestringrequired
Harmonized System code, 6–10 digits. Look one up in the app by description.
items[].quantityintegerrequired
Units of this item.
items[].valuenumberrequired
Value per unit.
items[].currencystring
ISO currency, defaults to USD.
items[].origin_countrystring
Where the item was made. Affects duty rates.
Core resources

Labels#

Buying a label charges your balance and returns a printable file, a tracker and, for international, a commercial invoice. Duties for DDP shipments and any insurance premium are charged with the label. Send an Idempotency-Key header so a retried request never buys the same label twice.

POST/v1/labelsBuy a label from a rate

Body parameters

rate_idstringrequired
A rate id from POST /v1/rates. Customs and insurance carry over from the quote.
destinationaddressrequired
Full ship-to: name, line_1, city, postal_code, country, plus phone for most international couriers. Validated before purchase.
label_formatenum
pdf_4x6 (default), pdf_letter, png or zpl.
referencestring
Your order number. Printed on the label and echoed in webhooks.
insurance.amountnumber
Override the insured value from the quote. Claims are filed in the app or API.
curlRequest
curl https://api.octoship.com/v1/labels \
  -H "Authorization: Bearer os_test_4eC39HqLyjWDarjt" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{
    "rate_id": "rate_7Dh3WxK",
    "label_format": "pdf_4x6",
    "reference": "#1042",
    "destination": {
      "name": "Lena Brandt",
      "line_1": "Invalidenstraße 117",
      "city": "Berlin",
      "postal_code": "10115",
      "country": "DE",
      "phone": "+49 30 1234567",
      "email": "[email protected]"
    }
  }'
jsonResponse · 201 Created
{
  "id": "lbl_3kTqZ9",
  "object": "label",
  "status": "purchased",
  "courier": "dhl",
  "service": "dhl_express_worldwide",
  "tracking_code": "1234567890",
  "tracker_id": "trk_7Vb1Ys",
  "amount": 31.80,
  "duties_and_taxes_paid": 21.81,
  "incoterms": "DDP",
  "insurance": { "amount": 92.00, "premium": 1.01 },
  "currency": "USD",
  "label_url": "https://files.octoship.com/labels/lbl_3kTqZ9.pdf",
  "commercial_invoice_url": "https://files.octoship.com/docs/inv_3kTqZ9.pdf",
  "label_format": "pdf_4x6",
  "reference": "#1042",
  "created_at": "2026-10-28T16:12:04Z"
}
Core resources

Pickups#

Schedule a courier pickup for one or more labels instead of dropping them off. Pickups are grouped per courier and address. Many are free; any fee is returned before you confirm.

POST/v1/pickupsSchedule a pickup

Body parameters

courierstringrequired
e.g. usps, ups, fedex, dhl.
label_idsstring[]required
Labels for that courier to hand over.
datedaterequired
Pickup date, YYYY-MM-DD, in the pickup address's time zone.
windowobject
start and end times. Defaults to the courier's standard window.
addressaddress
Defaults to your default ship-from address.
instructionsstring
Where to find the packages.
curlRequest
curl https://api.octoship.com/v1/pickups \
  -H "Authorization: Bearer os_test_4eC39HqLyjWDarjt" \
  -H "Content-Type: application/json" \
  -d '{
    "courier": "dhl",
    "label_ids": ["lbl_3kTqZ9", "lbl_8Wq1Rm", "lbl_2Pz7Kd"],
    "date": "2026-10-29",
    "window": { "start": "13:00", "end": "17:00" },
    "instructions": "Front desk, ask for shipping"
  }'
jsonResponse · 201 Created
{
  "id": "pck_6Tn2Hs",
  "object": "pickup",
  "status": "confirmed",
  "courier": "dhl",
  "confirmation_code": "CBJ261029004512",
  "date": "2026-10-29",
  "window": { "start": "13:00", "end": "17:00" },
  "label_count": 3,
  "amount": 0.00
}
Core resources

Returns#

Create a return label for an outbound shipment, swapping origin and destination for you. Set prepaid to send your buyer a ready-to-use label, and charge_on_use to pay only if it's actually scanned.

POST/v1/returnsCreate a return label

Body parameters

label_idstringrequired
The outbound label being returned.
servicestring
Return service. Defaults to the cheapest for the route.
prepaidboolean
Send a paid label (default true). Set false to let the buyer pay.
charge_on_useboolean
Only charge when the courier first scans the label.
reasonenum
wrong_size, damaged, not_as_described, changed_mind or other.
notify_emailstring
Email the label and a QR code to this address.
curlRequest
curl https://api.octoship.com/v1/returns \
  -H "Authorization: Bearer os_test_4eC39HqLyjWDarjt" \
  -H "Content-Type: application/json" \
  -d '{
    "label_id": "lbl_9Rk4Vb",
    "service": "usps_ground_advantage",
    "prepaid": true,
    "charge_on_use": true,
    "reason": "wrong_size",
    "notify_email": "[email protected]"
  }'
jsonResponse · 201 Created
{
  "id": "ret_4Hm8Lc",
  "object": "return",
  "status": "label_sent",
  "courier": "usps",
  "service": "usps_ground_advantage",
  "tracking_code": "9202090153540000123456",
  "label_url": "https://files.octoship.com/returns/ret_4Hm8Lc.pdf",
  "qr_code_url": "https://files.octoship.com/returns/ret_4Hm8Lc.png",
  "charge_on_use": true,
  "amount": 0.00
}
Core resources

Tracking#

Every label comes with a tracker. You can also create trackers for packages shipped elsewhere. Statuses are normalized across couriers and countries: pre_transit, in_transit, out_for_delivery, delivered, exception.

GET/v1/trackers/:idRetrieve a tracker

Path parameters

idstringrequired
Tracker id, e.g. trk_7Vb1Ys.
curlRequest
curl https://api.octoship.com/v1/trackers/trk_7Vb1Ys \
  -H "Authorization: Bearer os_test_4eC39HqLyjWDarjt"
jsonResponse · 200 OK
{
  "id": "trk_7Vb1Ys",
  "object": "tracker",
  "tracking_code": "1234567890",
  "courier": "dhl",
  "status": "out_for_delivery",
  "est_delivery_date": "2026-10-31",
  "public_url": "https://track.octoship.com/trk_7Vb1Ys",
  "events": [
    {
      "status": "out_for_delivery",
      "message": "Out for delivery",
      "location": "Berlin, DE",
      "occurred_at": "2026-10-31T07:42:00+01:00"
    },
    {
      "status": "in_transit",
      "message": "Customs clearance completed, duties prepaid (DDP)",
      "location": "Leipzig, DE",
      "occurred_at": "2026-10-30T14:05:00+01:00"
    },
    {
      "status": "pre_transit",
      "message": "Label created",
      "location": "Los Angeles, CA, US",
      "occurred_at": "2026-10-28T16:12:04-07:00"
    }
  ]
}
Events

Webhooks#

Instead of polling, register an HTTPS endpoint in Settings and we'll POST an event whenever something happens. Respond with any 2xx within 10 seconds; failed deliveries are retried with backoff for 3 days.

Events

  • label.createdA label was purchased, from the app or the API.
  • label.refundedA refund for an unused label was approved.
  • tracker.updatedA tracker got a new scan event or status.
  • tracker.deliveredThe package was delivered. Fires once per tracker.
  • tracker.exceptionThe courier reported a problem (address issue, customs hold, damage, return to sender).
  • pickup.confirmedThe courier confirmed a pickup and its time window.
  • return.label_usedA return label got its first scan. Charged now if charge_on_use is set.
  • claim.updatedAn insurance claim moved to reviewed, approved, paid or denied.
  • adjustment.createdThe courier adjusted postage after re-weighing or measuring.
jsonExample payload · tracker.updated
{
  "id": "evt_2Jd8Qa",
  "type": "tracker.updated",
  "created_at": "2026-10-31T11:15:02Z",
  "data": {
    "id": "trk_7Vb1Ys",
    "courier": "dhl",
    "tracking_code": "1234567890",
    "status": "delivered",
    "previous_status": "out_for_delivery",
    "reference": "#1042"
  }
}

Verifying signatures

Each request carries an Octoship-Signature header: a timestamp and an HMAC-SHA256 of timestamp.body using your endpoint's signing secret. Reject anything that doesn't match or is more than five minutes old.

jsverify.js
import crypto from "node:crypto";

// Octoship-Signature: t=1793358902,v1=5257a869e7ecebeda32a...
export function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  if (!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) {
    throw new Error("Bad signature");
  }
  return JSON.parse(rawBody);
}
Reference

Errors#

Octoship uses conventional HTTP status codes. Errors always return the same shape, with a human-readable message you can show to your team, and a stable code you can branch on.

jsonResponse · 400 Bad Request
{
  "error": {
    "type": "invalid_request_error",
    "code": "hs_code_missing",
    "message": "Item 2 needs an hs_code for shipments to DE.",
    "param": "customs.items[1].hs_code",
    "request_id": "req_Wm3oP1"
  }
}
StatusTypeMeaning
400invalid_request_errorA parameter is missing or malformed. Check param in the response.
401authentication_errorNo API key, or the key is revoked.
402payment_errorYour balance can't cover this label, its duties or insurance.
404not_foundThat rate, label, pickup, return or tracker doesn't exist (or belongs to another account).
409idempotency_errorThe same Idempotency-Key was reused with different parameters.
422courier_errorThe courier rejected the request, e.g. an undeliverable address or restricted item.
429rate_limit_errorSlow down. 100 requests per second per key.
5xxapi_errorSomething broke on our end. Safe to retry with the same Idempotency-Key.

Stuck? Ask a human.

Include the request_id and we'll find your request in seconds.

Contact supportwe reply fast

Grab a test key.

Sign up free, open Settings, and you'll be quoting rates from every courier in your terminal in two minutes.

Get your API key