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.
https://api.octoship.com/v1
01Get a test key
Settings → API keys. Test keys start with os_test_ and never charge you.
02Quote rates
POST a parcel and destination to /v1/rates. Get every courier, ranked.
03Buy 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.
Authentication#
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.
curl https://api.octoship.com/v1/account \
-H "Authorization: Bearer os_test_4eC39HqLyjWDarjt"Key types
os_test_…test mode- test mode
- Free, fake labels marked VOID. Rates are real-looking, nothing is charged.
os_live_…live mode- live mode
- Buys real labels from your balance. Rotate from Settings at any time.
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.
/v1/addresses/validateValidate an addressBody parameters
line_1stringrequired- string
- Street address.
line_2string- string
- Apartment, suite, unit.
citystringrequired- string
- City or locality.
statestring- string
- State, province or region, where the country uses one.
postal_codestring- string
- Postal or ZIP code.
countrystringrequired- string
- ISO 3166-1 alpha-2, e.g.
US,DE.
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"
}'{
"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"]
}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.
/v1/ratesGet rates for a parcelBody parameters
originaddressrequired- address
- Ship-from. At minimum
postal_codeandcountry. destinationaddressrequired- address
- Ship-to. Set
residentialfor accurate pricing. destination.countrystringrequired- string
- ISO country code. Anything other than the origin country makes it international and requires
customs. parcel.weight_oznumberrequired- number
- Weight in ounces, up to 2,400 (150 lb).
parcel.lengthnumber- number
- Inches. With width and height, enables dim weight and USPS Cubic pricing.
parcel.widthnumber- number
- Inches.
parcel.heightnumber- number
- Inches.
insurance.amountnumber- number
- Declared value to insure. The premium is returned on each rate.
couriersstring[]- string[]
- Limit to couriers, e.g.
usps,ups,fedex,dhl. Defaults to all. servicesstring[]- string[]
- Limit to services, e.g.
fedex_ground,dhl_express_worldwide. sort_byenum- enum
cheapest(default),fastestorbest_value.
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"
}'{
"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:
{
"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_internationalupsups_ground_saver, ups_ground, ups_2nd_day_air, ups_worldwide_saverfedexfedex_ground_economy, fedex_ground, fedex_2day, fedex_international_economydhldhl_express_worldwide, dhl_express_1200canada_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- enum
DDP(delivered duty paid) orDDU(delivered duty unpaid).contentsenum- enum
merchandise(default),gift,documents,sampleorreturn.items[].descriptionstringrequired- string
- Plain-language description, e.g. “Ceramic mug”.
items[].hs_codestringrequired- string
- Harmonized System code, 6–10 digits. Look one up in the app by description.
items[].quantityintegerrequired- integer
- Units of this item.
items[].valuenumberrequired- number
- Value per unit.
items[].currencystring- string
- ISO currency, defaults to
USD. items[].origin_countrystring- string
- Where the item was made. Affects duty rates.
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.
/v1/labelsBuy a label from a rateBody parameters
rate_idstringrequired- string
- A rate id from
POST /v1/rates. Customs and insurance carry over from the quote. destinationaddressrequired- address
- Full ship-to: name, line_1, city, postal_code, country, plus phone for most international couriers. Validated before purchase.
label_formatenum- enum
pdf_4x6(default),pdf_letter,pngorzpl.referencestring- string
- Your order number. Printed on the label and echoed in webhooks.
insurance.amountnumber- number
- Override the insured value from the quote. Claims are filed in the app or API.
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]"
}
}'{
"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"
}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.
/v1/pickupsSchedule a pickupBody parameters
courierstringrequired- string
- e.g.
usps,ups,fedex,dhl. label_idsstring[]required- string[]
- Labels for that courier to hand over.
datedaterequired- date
- Pickup date, YYYY-MM-DD, in the pickup address's time zone.
windowobject- object
startandendtimes. Defaults to the courier's standard window.addressaddress- address
- Defaults to your default ship-from address.
instructionsstring- string
- Where to find the packages.
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"
}'{
"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
}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.
/v1/returnsCreate a return labelBody parameters
label_idstringrequired- string
- The outbound label being returned.
servicestring- string
- Return service. Defaults to the cheapest for the route.
prepaidboolean- boolean
- Send a paid label (default
true). Setfalseto let the buyer pay. charge_on_useboolean- boolean
- Only charge when the courier first scans the label.
reasonenum- enum
wrong_size,damaged,not_as_described,changed_mindorother.notify_emailstring- string
- Email the label and a QR code to this address.
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]"
}'{
"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
}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.
/v1/trackers/:idRetrieve a trackerPath parameters
idstringrequired- string
- Tracker id, e.g.
trk_7Vb1Ys.
curl https://api.octoship.com/v1/trackers/trk_7Vb1Ys \
-H "Authorization: Bearer os_test_4eC39HqLyjWDarjt"{
"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"
}
]
}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.
{
"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.
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);
}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.
{
"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"
}
}| Status | Type | Meaning |
|---|---|---|
| 400 | invalid_request_error | A parameter is missing or malformed. Check param in the response. |
| 401 | authentication_error | No API key, or the key is revoked. |
| 402 | payment_error | Your balance can't cover this label, its duties or insurance. |
| 404 | not_found | That rate, label, pickup, return or tracker doesn't exist (or belongs to another account). |
| 409 | idempotency_error | The same Idempotency-Key was reused with different parameters. |
| 422 | courier_error | The courier rejected the request, e.g. an undeliverable address or restricted item. |
| 429 | rate_limit_error | Slow down. 100 requests per second per key. |
| 5xx | api_error | Something 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.










