ShipAgent360 API
The ShipAgent360 API lets you integrate real-time multi-carrier shipping rates, label generation, and shipment tracking directly into your application. Get rates from FedEx, UPS, GLS, and Roadie, book shipments, and track packages — all through a single REST API.
https://api.shipagent360.com/api/v1/publicAuthentication
All API requests require a valid API key passed as a Bearer token in the Authorization header. API keys are scoped to your organization and have a rate limit of 1,000 requests per hour.
Keep your API key secret. Never expose it in client-side code or public repositories. If a key is compromised, revoke it immediately from your account settings.
# Pass your API key in the Authorization header
curl https://api.shipagent360.com/api/v1/public/me \
-H "Authorization: Bearer sa_live_YOUR_API_KEY"Getting an API key
- Log in to ShipAgent360
- Contact your ShipAgent360 account manager to generate an API key for your organization
- Copy the key when shown — it will only be displayed once
- Store it securely as an environment variable:
SHIPAGENT360_API_KEY
Quickstart
Get your first shipment booked in under 5 minutes.
Verify your API key
Confirm your key is working and check your rate limit status.
Get rates
Pass shipper, recipient, and package details to get real-time carrier rates.
Book the shipment
Send the quote's rateId with its carrierId and serviceCode to book at exactly the quoted price and get a label.
Complete example
# Step 1 — Verify key
curl https://api.shipagent360.com/api/v1/public/me -H "Authorization: Bearer sa_live_YOUR_KEY"
# Step 2 — Get rates
curl -X POST https://api.shipagent360.com/api/v1/public/rates \
-H "Authorization: Bearer sa_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"shipper":{"name":"Sender","street1":"123 Main St","city":"Los Angeles","state":"CA","zip":"90210","phone":"3105550000"},"recipient":{"name":"Receiver","street1":"456 Park Ave","city":"New York","state":"NY","zip":"10001","phone":"2125550000","isResidential":false},"packages":[{"weightLbs":5,"lengthIn":12,"widthIn":10,"heightIn":8}]}'
# Step 3 — Book (use carrierId + serviceCode from step 2)
curl -X POST https://api.shipagent360.com/api/v1/public/shipments \
-H "Authorization: Bearer sa_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"rateId":"rate_abc123","carrierId":"FEDEX","serviceCode":"GROUND_HOME_DELIVERY","shipper":{...},"recipient":{...},"packages":[{...}],"reference":"ORDER-001"}'/ratesGet Rates
Returns real-time shipping rates from all enabled carriers. Rates are sorted by price ascending. Roadie rates are only returned when the delivery is within 40 miles of the pickup location. GLS rates are only returned when the shipper.zip falls inside the GLS pickup area — see GLS Pickup Area.
When you request a signature, every quote carries a signatureAvailable flag. GLS cannot be asked for a signature, so GLS quotes come back withfalse: the price is real and bookable, but the parcel will be delivered without a signature. Check the flag before you present GLS as an option.
A carrier that cannot price a given shipment is omitted from the response entirely — no quote and no entry in errors, which is always an empty array. Read quotes to see which carriers are available for a shipment; a carrier missing from it simply has no rate to offer.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| shipper | object | required | Origin address |
| shipper.name | string | required | Sender name or company |
| shipper.street1 | string | required | Street address |
| shipper.city | string | required | City |
| shipper.state | string | required | 2-letter state code |
| shipper.zip | string | required | ZIP code |
| shipper.phone | string | required | 10-digit phone number |
| recipient | object | required | Destination address (same fields as shipper) |
| recipient.isResidential | boolean | optional | Whether delivery is to a residence (default: false) |
| signature | string | optional | Delivery signature for the whole shipment: NONE (default), REQUIRED, or ADULT. A signature is a priced carrier service, so send the same value here and on POST /shipments — the carrier’s charge is already included in the price returned. REQUIRED maps to FedEx Indirect and UPS Signature Required; ADULT to FedEx Adult and UPS Adult Signature Required. |
| packages | array | required | One object per package |
| packages[].weightLbs | number | required | Weight in pounds |
| packages[].lengthIn | number | optional | Length in inches (default: 12) |
| packages[].widthIn | number | optional | Width in inches (default: 10) |
| packages[].heightIn | number | optional | Height in inches (default: 8) |
| packages[].declaredValue | number | optional | Declared value in USD. Sent to the carrier on both the rate and the booking, so the carrier’s declared-value (insurance) charge is already included in the price returned by /rates and in the amount billed at booking — it is not added separately afterwards. Omit for standard carrier liability only. |
| packages[].packagingType | string | optional | Packaging type: MY_PACKAGING (default), LOOSE, ENVELOPE, PAK, SMALL_BOX, MEDIUM_BOX, LARGE_BOX, or TUBE. Affects FedEx and UPS pricing so quotes match the bill; GLS and Roadie ignore it. LOOSE means the item ships unpackaged (e.g. tires) and is rated by the dimensions you enter. For carrier-branded types, dimensions are optional. |
curl -X POST https://api.shipagent360.com/api/v1/public/rates \
-H "Authorization: Bearer sa_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"shipper": {
"name": "Acme Corp",
"street1": "123 Main St",
"city": "Los Angeles",
"state": "CA",
"zip": "90210",
"phone": "3105550000"
},
"recipient": {
"name": "Jane Smith",
"street1": "456 Park Ave",
"city": "New York",
"state": "NY",
"zip": "10001",
"phone": "2125550000",
"isResidential": false
},
"packages": [
{
"weightLbs": 5.2,
"lengthIn": 12,
"widthIn": 10,
"heightIn": 8,
"declaredValue": 150,
"packagingType": "MY_PACKAGING"
}
]
}'Response
{
"quotes": [
{
"rateId": "rate_abc123",
"carrierId": "FEDEX",
"serviceCode": "GROUND_HOME_DELIVERY",
"serviceName": "FedEx Ground Home Delivery",
"price": 18.45,
"currency": "USD",
"estimatedDelivery": "2026-07-06",
"transitDays": 3,
"signatureAvailable": false
},
{
"rateId": "rate_def456",
"carrierId": "UPS",
"serviceCode": "03",
"serviceName": "UPS Ground",
"price": 19.12,
"currency": "USD",
"estimatedDelivery": "2026-07-07",
"transitDays": 4
},
{
"rateId": "rate_ghi789",
"carrierId": "GLS",
"serviceCode": "CPS",
"serviceName": "GLS Ground",
"price": 17.80,
"currency": "USD",
"estimatedDelivery": "2026-07-06",
"transitDays": 3
},
{
"rateId": "rate_jkl012",
"carrierId": "ROADIE",
"serviceCode": "ROADIE_SAME_DAY",
"serviceName": "Roadie Same-Day Delivery (8.2 mi)",
"price": 14.00,
"currency": "USD",
"estimatedDelivery": "2026-07-02",
"transitDays": 0
}
],
"errors": []
}/shipmentsBook Shipment
Books a shipment with the chosen carrier and returns a tracking number and a labelUrl. Use the carrierId and serviceCode from a rates response. The labelUrl is a link to the Download Label endpoint below — fetch it with your API key to retrieve the label file.
Roadie shipments do not generate a shipping label — the driver is notified via the Roadie app. The response will include a tracking number but labelUrl will be null.
Booking against a quote. Send the rateId from the quote your customer chose and you are billed exactly the price that quote showed — prices are held on our side, so nothing about the amount travels in your request. A rateId is valid for 30 minutes and can book one shipment; an expired or already-used one is rejected rather than silently re-priced. If you omit it, the shipment is re-rated at current prices, which may differ from what you were quoted earlier.
Choosing a label format. Send labelFormat as "PDF" or "ZPL". This has to be decided here, at booking time: a ZPL label can be rendered to PDF, but a PDF can never be turned back into ZPL. Omit the field and your account default is used — call GET /me to see it, or ask your account manager to change it.
Additional request fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| rateId | string | optional | The rateId of the quote you are booking, from the /rates response. Guarantees you are billed exactly the price that quote showed. Valid for 30 minutes and usable once. Omit it and the shipment is re-rated at current prices instead. |
| carrierId | string | required | Carrier ID from rates response (FEDEX, UPS, GLS, ROADIE, FEDEX_SAMEDAY). Booking GLS from a ZIP outside its pickup area returns 400 — see GLS Pickup Area. |
| serviceCode | string | required | Service code from rates response |
| reference | string | optional | Your PO, work order, or reference number. Printed on the shipping label for all carriers (FedEx, UPS, GLS) so you can match the label to your order. Max 30 characters. |
| shipper | object | required | Same as rates request |
| recipient | object | required | Same as rates request |
| packages | array | required | Same as rates request |
| labelFormat | string | optional | PDF or ZPL. PDF prints on a standard printer; ZPL is a 4x6 thermal label. Must be sent at booking time — a label cannot be converted to ZPL later. Omit to use your account default (see Verify Key). Roadie produces no label in either format. |
| idempotencyKey | string | optional | Optional replay key, unique per booking on your side. Also accepted as the Idempotency-Key header. Send the same key again — after a timeout, a retry, or a double-submit — and you get back the shipment created the first time instead of a second shipment and a second charge. Keys are scoped to your account. |
| returnAddress | object | optional | Where the carrier should send this shipment if it cannot be delivered, when that is not your ship-from address. Same field shape as shipper, but street1, city, state and zip are all required when the object is present. Omit it and your account default return address is used if you have one; send null to use the ship-from address for this shipment only. Not accepted on /rates — a return address never changes the price. |
Return addresses: what each carrier does
returnAddress is not a return label — it is the sender block on this outbound label, and the carriers do not treat it the same way.
- UPS prints it and returns undeliverable packages to it. UPS prints only the first street line, trims the name at 30 characters and the city at 15, so we fold
street2into the first line. - FedEx prints it in place of the label’s return block. FedEx does not document how an undeliverable package is routed, so treat it as printed, not guaranteed. A name or company is required; if you send neither we fall back to your shipper name.
- GLS prints it. Pickup and undeliverable routing both still follow your ship-from address.
- Roadie produces no carrier label, so the field is ignored.
A return address is part of the idempotency fingerprint: replaying a key with a different return address is treated as a different booking, not a retry.
Retrying safely
If a booking request times out you cannot tell whether the shipment was created. Send an Idempotency-Key header (or idempotencyKey in the body) on the first attempt and reuse the same value on every retry. The first request books; every retry returns that same shipment, with the same tracking number and the same charge. Without a key each retry is treated as a new booking.
curl -X POST https://api.shipagent360.com/api/v1/public/shipments \
-H "Authorization: Bearer sa_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rateId": "rate_abc123",
"carrierId": "FEDEX",
"serviceCode": "GROUND_HOME_DELIVERY",
"shipper": {
"name": "Acme Corp",
"street1": "123 Main St",
"city": "Los Angeles",
"state": "CA",
"zip": "90210",
"phone": "3105550000"
},
"recipient": {
"name": "Jane Smith",
"street1": "456 Park Ave",
"city": "New York",
"state": "NY",
"zip": "10001",
"phone": "2125550000",
"isResidential": false
},
"packages": [
{
"weightLbs": 5.2,
"lengthIn": 12,
"widthIn": 10,
"heightIn": 8,
"declaredValue": 150
}
],
"reference": "ORDER-12345",
"labelFormat": "PDF"
}'Response
{
"id": "shp_abc123",
"trackingNumber": "794835001234",
"status": "BOOKED",
"carrierId": "FEDEX",
"serviceCode": "GROUND_HOME_DELIVERY",
"price": 18.45,
"labelUrl": "https://api.shipagent360.com/v1/public/shipments/shp_abc123/label",
"estimatedDelivery": "2026-07-06",
"createdAt": "2026-07-01T14:23:00Z"
}carrierId: GLSGLS Pickup Area
GLS delivers anywhere in the United States but only collects parcels from a limited set of ZIP codes across the western states. The restriction applies to the origin only — the destination can be anywhere in the US.
// POST /shipments — GLS booked from an origin outside the pickup area
{
"statusCode": 400,
"message": "GLS does not offer pickup service from ZIP 10001. Choose a different carrier or ship from an address inside GLS's pickup area.",
"error": "Bad Request"
}carrierId: GLSGLS Driver Pickup
FedEx and UPS collect against a standing daily pickup, and Roadie dispatches its own driver when you book. GLS does neither: a GLS label alone does not summon anyone. ShipAgent360 therefore requests an on-call GLS pickup for you automatically whenever you book a GLS shipment — you do not need to call GLS or request one through the API.
// POST /shipments — a booked GLS shipment carries its pickup
{
"id": "shp_abc123",
"status": "BOOKED",
"carrierId": "GLS",
"serviceCode": "CPS",
"trackingNumber": "12345678",
"customerPrice": 18.75,
"pickupStatus": "SCHEDULED",
"pickupConfirmationNumber": "4204661",
"pickupDate": "2026-09-04",
"pickupWindowStart": "08:00 AM",
"pickupWindowEnd": "07:00 PM"
}
// The label printed but GLS would not take the pickup. The shipment is still
// booked and the label is still valid — we retry automatically.
{
"id": "shp_abc124",
"status": "BOOKED",
"trackingNumber": "12345679",
"pickupStatus": "FAILED",
"pickupConfirmationNumber": null,
"pickupError": "GLS pickup was not scheduled — 77: Zip not serviced"
}carrierId: ROADIERoadie Same-Day Delivery
Roadie is a same-day local delivery service using gig drivers. It differs from traditional carriers in a few important ways.
// Roadie rate quote — only returned for deliveries within 40 miles
{
"carrierId": "ROADIE",
"serviceCode": "ROADIE_SAME_DAY",
"serviceName": "Roadie Same-Day Delivery (8.2 mi)",
"price": 14.00,
"currency": "USD",
"transitDays": 0
}
// Roadie booking response — note labelUrl is null
{
"id": "shp_xyz789",
"trackingNumber": "1847392",
"status": "BOOKED",
"carrierId": "ROADIE",
"serviceCode": "ROADIE_SAME_DAY",
"price": 14.00,
"labelUrl": null
}carrierId: FEDEX_SAMEDAYFedEx SameDay Local
FedEx SameDay Local is a local courier service: a FedEx driver collects from your pickup address and delivers the same day. Like Roadie, it works differently from parcel shipping.
// FedEx SameDay Local rate quote
{
"carrierId": "FEDEX_SAMEDAY",
"serviceCode": "FEDEX_SDL_2_HOUR",
"serviceName": "FedEx SameDay Local — 2-Hour Window",
"price": 24.50,
"currency": "USD",
"estimatedDays": 0
}/shipmentsList Shipments
Returns a paginated list of shipments for your organization.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | number | optional | Page number (default: 1) |
| limit | number | optional | Results per page, max 100 (default: 20) |
| status | string | optional | Filter by status: BOOKED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, EXCEPTION, CANCELLED |
| search | string | optional | Search by tracking number, recipient name, city, or ZIP |
| dateFrom | string | optional | Filter by created date from (ISO 8601: YYYY-MM-DD) |
| dateTo | string | optional | Filter by created date to (ISO 8601: YYYY-MM-DD) |
curl "https://api.shipagent360.com/api/v1/public/shipments?page=1&limit=20&status=IN_TRANSIT" \
-H "Authorization: Bearer sa_live_YOUR_API_KEY"Response
{
"shipments": [ /* array of shipment objects */ ],
"total": 142,
"page": 1,
"limit": 20,
"pages": 8
}/shipments/:idGet Shipment
Returns full details for a single shipment including current status and tracking events.
curl https://api.shipagent360.com/api/v1/public/shipments/shp_abc123 \
-H "Authorization: Bearer sa_live_YOUR_API_KEY"/shipments/:id/labelDownload Label
Downloads the shipping label for a booked shipment. This is the same URL returned as labelUrl when you book — authenticate with your API key just like any other endpoint.
The response is the raw label file, not JSON, in whichever format the shipment was booked with. Always read the Content-Type and Content-Disposition response headers rather than assuming a PDF.
| Shipment | Content-Type | You receive |
|---|---|---|
| 1 package, PDF | application/pdf | A .pdf file |
| 1 package, ZPL | text/plain | A .zpl file |
| Multiple packages, all PDF | application/pdf | One merged PDF, one page per package |
| Multiple packages, all ZPL | text/plain | One .zpl stream containing every label |
| Mixed formats | application/zip | A .zip with one file per package |
Roadie and FedEx SameDay Local shipments have no label, so this endpoint returns 404 Not Found with the message “No label available for this shipment.”
# The file you get back depends on how the shipment was booked.
# -OJ writes it under the filename the API sends (.pdf, .zpl or .zip).
curl -L -OJ https://api.shipagent360.com/api/v1/public/shipments/shp_abc123/label \
-H "Authorization: Bearer sa_live_YOUR_API_KEY" \
/shipments/:id/trackingTrack a Shipment
Current status and the full carrier event history, newest first. Queries the carrier live. If the carrier is unreachable the last stored events are returned rather than an error, so this is safe to poll.
curl https://api.shipagent360.com/api/v1/public/shipments/shp_abc123/tracking \
-H "Authorization: Bearer sa_live_YOUR_API_KEY"Response
{
"status": "IN_TRANSIT",
"events": [
{
"timestamp": "2026-08-28T14:22:00.000Z",
"status": "IN_TRANSIT",
"description": "Departed FedEx location",
"location": "Memphis, TN"
},
{
"timestamp": "2026-08-27T22:05:00.000Z",
"status": "PICKED_UP",
"description": "Picked up",
"location": "Los Angeles, CA"
}
]
}/track/:trackingNumberThe same payload, addressed by the carrier tracking number rather than our shipment id — which is usually what your own systems store. Accepts the master tracking number or any individual package number on a multi-package shipment; spaces are ignored. Only numbers belonging to your organization resolve.
curl https://api.shipagent360.com/api/v1/public/track/794835001234 \
-H "Authorization: Bearer sa_live_YOUR_API_KEY"{
"shipmentId": "shp_abc123",
"status": "DELIVERED",
"events": [ ... ]
}Polling. We refresh tracking on a schedule of our own, so polling more than every 15 minutes per shipment will mostly return the same events and counts against your rate limit. Shipments in a terminal state (DELIVERED, CANCELLED) will not change again.
/webhooksWebhooks
We POST to your endpoint when a shipment’s status changes, so you don’t have to poll. Register an endpoint from your dashboard or through the API. Every payload is signed, and failed deliveries are retried.
Events
| Parameter | Type | Required | Description |
|---|---|---|---|
| shipment.delivered | event | optional | The carrier reports the shipment delivered. Sent once, on the transition. |
| shipment.exception | event | optional | The carrier reports a problem — failed delivery attempt, address issue, damage. |
| shipment.in_transit | event | optional | The shipment is moving, or out for delivery. |
Register an endpoint
curl -X POST https://api.shipagent360.com/api/v1/webhooks \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '{"url":"https://hooks.yourcompany.com/shipagent","events":["shipment.delivered"]}'The signing secret is returned once. It is the only way to prove a payload came from us — store it before you close the response. We cannot show it again; if you lose it, delete the endpoint and register a new one. The URL must be https and publicly reachable.
What we send
POST https://hooks.yourcompany.com/shipagent
X-ShipAgent360-Event: shipment.delivered
X-ShipAgent360-Delivery: 4c1e...
X-ShipAgent360-Timestamp: 1787900000
X-ShipAgent360-Signature: 9f86d0818...
{
"event": "shipment.delivered",
"createdAt": "2026-08-28T18:04:00.000Z",
"data": {
"shipmentId": "shp_abc123",
"status": "DELIVERED",
"previousStatus": "OUT_FOR_DELIVERY",
"trackingNumber": "794835001234",
"trackingNumbers": ["794835001234"],
"carrier": "FEDEX",
"reference": "ORDER-12345",
"recipient": { "name": "Jane Smith", "city": "New York", "state": "NY", "zip": "10001" },
"deliveredAt": "2026-08-28T18:03:58.000Z"
}
}Verifying the signature
The signature is HMAC-SHA256(secret, "{timestamp}.{raw body}"), hex-encoded. Sign the raw body, before any JSON parsing — re-serialising changes the bytes and the signature will not match. Compare in constant time.
const crypto = require('crypto')
app.post('/shipagent', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-ShipAgent360-Timestamp')
const signature = req.get('X-ShipAgent360-Signature')
const expected = crypto
.createHmac('sha256', process.env.SHIPAGENT_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`)
.digest('hex')
const ok = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
if (!ok) return res.sendStatus(401)
// Reject a timestamp far from now to stop replay of a captured payload.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(401)
const event = JSON.parse(req.body)
// Respond fast; do the work afterwards. We time out after 10 seconds.
res.sendStatus(200)
})Delivery and retries
- Any 2xx is success. Anything else is retried after 1, 5, 30, 120 and 360 minutes, then marked failed.
- We time out after 10 seconds. Acknowledge first and process asynchronously.
- Redirects are not followed.
- An endpoint that fails 15 times in a row is disabled; re-enable it from your dashboard.
- Delivery is at least once. Retries and replica overlap mean you can receive a duplicate — key on
X-ShipAgent360-Deliveryand ignore ids you have already handled. GET /webhooks/deliverieslists recent attempts with response codes, so “we never got it” is answerable.
/meVerify API Key
Verifies your API key is valid and returns your organization ID, current rate limit usage, and the label format your bookings use when they do not specify one. Useful for testing and monitoring.
curl https://api.shipagent360.com/api/v1/public/me \
-H "Authorization: Bearer sa_live_YOUR_API_KEY"Response
{
"orgId": "e96cc419-ddbf-4f59-bc0b-ea2856bfbe65",
"requestsThisHour": 42,
"rateLimit": 1000,
"defaultLabelFormat": "PDF",
"creditLimitApplies": true,
"creditLimit": 5000.00,
"balance": 2000.00,
"availableCredit": 3000.00
}Errors
ShipAgent360 uses standard HTTP status codes. All error responses include a message field.
| Status | Code | Meaning |
|---|---|---|
| 200 | OK | Request succeeded |
| 400 | Bad Request | Invalid request body or missing required fields |
| 401 | Unauthorized | Missing or invalid API key |
| 402 | Payment Required | Credit limit reached, or a card charge failed at booking. See below |
| 404 | Not Found | Resource not found or not accessible by your organization |
| 429 | Too Many Requests | Rate limit exceeded (1,000 req/hr). Check Retry-After header |
| 500 | Server Error | Something went wrong on our end — contact support |
Error response format
{
"statusCode": 401,
"message": "Invalid or expired API key",
"error": "Unauthorized"
}Rate limit response
{
"statusCode": 429,
"message": "Rate limit exceeded",
"limit": 1000,
"windowSeconds": 3600,
"retryAfterSeconds": 1423
}402 — credit limit reached
Returned by POST /shipments when the shipment would take your account over its credit limit. No shipment is created and no carrier is contacted. If a card is on file we attempt to collect the outstanding balance first, so this only appears when that is not possible. A statement covering everything unbilled is emailed at the same time. Poll GET /me to see the balance before you hit it.
{
"statusCode": 402,
"error": "Credit limit reached",
"message": "This shipment would put your account over its $5000.00 credit limit ($4900.00 outstanding, $100.00 available). Pay down your balance to continue shipping.",
"balance": 4900.00,
"limit": 5000.00,
"available": 100.00,
"statementInvoiceNumber": "SA-2026-00042"
}400 — rate quote not usable
A rateId is valid for 30 minutes and books one shipment. It is also tied to the exact shipment it was quoted for, so changing a weight, dimension, declared value or either ZIP invalidates it. We refuse these rather than re-pricing silently — booking you at a number you were never shown would be worse than an error. Request fresh rates and book again.
{
"statusCode": 400,
"message": "This rate quote has expired. Request fresh rates and book again.",
"error": "Bad Request"
}