ShipAgent360 API v1

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.

Real-time rates
Live rates from FedEx, UPS, GLS, and Roadie same-day delivery
Label generation
Book and get shipping labels in a single API call
REST API
Simple JSON over HTTPS — works with any language
API keys
Secure per-organization keys with rate limiting
Base URL
https://api.shipagent360.com/api/v1/public

Authentication

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.

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

  1. Log in to ShipAgent360
  2. Contact your ShipAgent360 account manager to generate an API key for your organization
  3. Copy the key when shown — it will only be displayed once
  4. Store it securely as an environment variable: SHIPAGENT360_API_KEY

Quickstart

Get your first shipment booked in under 5 minutes.

1

Verify your API key

Confirm your key is working and check your rate limit status.

2

Get rates

Pass shipper, recipient, and package details to get real-time carrier rates.

3

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

bash
# 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"}'
POST/rates

Get 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

ParameterTypeRequiredDescription
shipperobjectrequiredOrigin address
shipper.namestringrequiredSender name or company
shipper.street1stringrequiredStreet address
shipper.citystringrequiredCity
shipper.statestringrequired2-letter state code
shipper.zipstringrequiredZIP code
shipper.phonestringrequired10-digit phone number
recipientobjectrequiredDestination address (same fields as shipper)
recipient.isResidentialbooleanoptionalWhether delivery is to a residence (default: false)
signaturestringoptionalDelivery 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.
packagesarrayrequiredOne object per package
packages[].weightLbsnumberrequiredWeight in pounds
packages[].lengthInnumberoptionalLength in inches (default: 12)
packages[].widthInnumberoptionalWidth in inches (default: 10)
packages[].heightInnumberoptionalHeight in inches (default: 8)
packages[].declaredValuenumberoptionalDeclared 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[].packagingTypestringoptionalPackaging 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
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

json
{
  "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": []
}
POST/shipments

Book 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

ParameterTypeRequiredDescription
rateIdstringoptionalThe 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.
carrierIdstringrequiredCarrier 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.
serviceCodestringrequiredService code from rates response
referencestringoptionalYour 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.
shipperobjectrequiredSame as rates request
recipientobjectrequiredSame as rates request
packagesarrayrequiredSame as rates request
labelFormatstringoptionalPDF 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.
idempotencyKeystringoptionalOptional 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.
returnAddressobjectoptionalWhere 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 street2 into 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
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

json
{
  "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"
}
NOTEcarrierId: GLS

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

Origin only
Only shipper.zip is checked. recipient.zip is never restricted — GLS delivers nationwide.
Rates
When shipper.zip is outside the pickup area, GLS is omitted from the /rates response. No quote is returned and no entry appears in errors.
Booking
POST /shipments with carrierId GLS and an out-of-area shipper.zip returns 400 Bad Request before any charge is made. The same applies to bulk booking.
ZIP+4
ZIP+4 values are accepted and matched on the first five digits. A missing or non-US postal code is treated as out of area.
Coverage
Currently AZ, CA, CO, ID, NV, OR, UT and WA. The list is maintained by ShipAgent360 and changes as GLS expands — always rate before you book.
json
// 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"
}
NOTEcarrierId: GLS

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

Automatic
One pickup is requested per GLS shipment, immediately after the shipment is booked and paid. There is no request field to set and no extra call to make.
When
Pickups are requested for 3:00 PM local time at the pickup address. Book before 11:00 AM local on a weekday and the driver comes at 3:00 PM that day; book at or after 11:00 AM and it is 3:00 PM the next business day. Bookings from Friday 11:00 AM to Monday 11:00 AM are collected Monday at 3:00 PM.
Confirmation
A booked GLS shipment carries pickupStatus, pickupConfirmationNumber, pickupDate, pickupWindowStart and pickupWindowEnd. Quote the confirmation number to GLS if you need to ask about a collection.
If it fails
Your label is still valid. pickupStatus is FAILED with pickupError, and we retry in the background. If it stays FAILED, drop the package at any GLS location or call GLS to arrange collection.
Cancelling
Cancelling a GLS shipment releases its pickup too. A pickup already scheduled for TODAY cannot be recalled — GLS hands same-day requests straight to field operations.
Other carriers
pickupStatus is null on FedEx, UPS and Roadie shipments. These fields are GLS-only.
json
// 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"
}
NOTEcarrierId: ROADIE

Roadie Same-Day Delivery

Roadie is a same-day local delivery service using gig drivers. It differs from traditional carriers in a few important ways.

40-mile radius
Roadie rates are only returned when the delivery address is within 40 miles of the pickup address. Requests outside this radius return no Roadie quote.
No shipping label
Roadie does not generate a shipping label. The assigned driver is notified through the Roadie app with pickup and delivery details.
Same-day delivery
Roadie shipments are fulfilled the same day. The pickup window opens 2 hours after booking and delivery is completed within 8 hours.
Service code
Always use ROADIE_SAME_DAY as the serviceCode when booking a Roadie shipment.
json
// 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
}
NOTEcarrierId: FEDEX_SAMEDAY

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

Local lanes only
Quotes are returned only when FedEx SameDay Local serves both the pickup and delivery address. A full street address is required on both ends — a ZIP-only request returns no quote.
Three service levels
FEDEX_SDL_SAME_DAY (delivered today), FEDEX_SDL_4_HOUR (4-hour delivery window) and FEDEX_SDL_2_HOUR (2-hour delivery window). Only the levels available for the lane are returned.
No shipping label
No label is generated and labelFormat is ignored. The driver is dispatched when the shipment is booked. The download label endpoint returns 404.
Signature
signature REQUIRED and ADULT both request a signature on delivery. FedEx SameDay Local has no separate adult-signature tier.
Phone numbers
Include a phone number for the shipper and the recipient so the driver can reach them. Deliveries without one may be rejected by FedEx.
Tracking
Status updates (driver assigned, picked up, out for delivery, delivered) arrive in real time and fire the usual shipment webhooks.
json
// 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
}
GET/shipments

List Shipments

Returns a paginated list of shipments for your organization.

Query parameters

ParameterTypeRequiredDescription
pagenumberoptionalPage number (default: 1)
limitnumberoptionalResults per page, max 100 (default: 20)
statusstringoptionalFilter by status: BOOKED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, EXCEPTION, CANCELLED
searchstringoptionalSearch by tracking number, recipient name, city, or ZIP
dateFromstringoptionalFilter by created date from (ISO 8601: YYYY-MM-DD)
dateTostringoptionalFilter by created date to (ISO 8601: YYYY-MM-DD)
curl
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

json
{
  "shipments": [ /* array of shipment objects */ ],
  "total": 142,
  "page": 1,
  "limit": 20,
  "pages": 8
}
GET/shipments/:id

Get Shipment

Returns full details for a single shipment including current status and tracking events.

curl
curl https://api.shipagent360.com/api/v1/public/shipments/shp_abc123 \
  -H "Authorization: Bearer sa_live_YOUR_API_KEY"
GET/shipments/:id/label

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

ShipmentContent-TypeYou receive
1 package, PDFapplication/pdfA .pdf file
1 package, ZPLtext/plainA .zpl file
Multiple packages, all PDFapplication/pdfOne merged PDF, one page per package
Multiple packages, all ZPLtext/plainOne .zpl stream containing every label
Mixed formatsapplication/zipA .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.”

curl
# 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" \
GET/shipments/:id/tracking

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

bash
curl https://api.shipagent360.com/api/v1/public/shipments/shp_abc123/tracking \
  -H "Authorization: Bearer sa_live_YOUR_API_KEY"

Response

json
{
  "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"
    }
  ]
}
GET/track/:trackingNumber

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

bash
curl https://api.shipagent360.com/api/v1/public/track/794835001234 \
  -H "Authorization: Bearer sa_live_YOUR_API_KEY"
json
{
  "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.

POST/webhooks

Webhooks

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

ParameterTypeRequiredDescription
shipment.deliveredeventoptionalThe carrier reports the shipment delivered. Sent once, on the transition.
shipment.exceptioneventoptionalThe carrier reports a problem — failed delivery attempt, address issue, damage.
shipment.in_transiteventoptionalThe shipment is moving, or out for delivery.

Register an endpoint

bash
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

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

javascript
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-Delivery and ignore ids you have already handled.
  • GET /webhooks/deliveries lists recent attempts with response codes, so “we never got it” is answerable.
GET/me

Verify 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
curl https://api.shipagent360.com/api/v1/public/me \
  -H "Authorization: Bearer sa_live_YOUR_API_KEY"

Response

json
{
  "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.

StatusCodeMeaning
200OKRequest succeeded
400Bad RequestInvalid request body or missing required fields
401UnauthorizedMissing or invalid API key
402Payment RequiredCredit limit reached, or a card charge failed at booking. See below
404Not FoundResource not found or not accessible by your organization
429Too Many RequestsRate limit exceeded (1,000 req/hr). Check Retry-After header
500Server ErrorSomething went wrong on our end — contact support

Error response format

json
{
  "statusCode": 401,
  "message": "Invalid or expired API key",
  "error": "Unauthorized"
}

Rate limit response

json
{
  "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.

json
{
  "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.

json
{
  "statusCode": 400,
  "message": "This rate quote has expired. Request fresh rates and book again.",
  "error": "Bad Request"
}