REST API + webhooks for shipping automation
Build shiponline.app into your own product. Quote multi-carrier rates, buy and refund labels programmatically, subscribe to shipment events via webhook. Bearer-token auth, JSON in/out, no extra per-label cost on top of the standard pricing.
Quick start
Three steps from zero to a printed label:
- Sign up at shiponline.app (free, no card required).
- Visit Settings → API tokens, create a token with the
write:shipmentsscope, copy the secret (shown once). - Buy your first label with one POST request:
curl -X POST https://shiponline.app/api/v1/labels/create \
-H "Authorization: Bearer shp_live_..." \
-H "Content-Type: application/json" \
-d '{
"fromAddress": {
"street1": "123 Main St",
"city": "San Francisco",
"state": "CA",
"zip": "94103",
"country": "US"
},
"toAddress": {
"street1": "350 Fifth Avenue",
"city": "New York",
"state": "NY",
"zip": "10118",
"country": "US"
},
"parcel": {
"lengthIn": 10,
"widthIn": 8,
"heightIn": 4,
"weightOz": 16
},
"serviceCategory": "standard"
}'The response returns a Shipment object with status: "paying". Poll GET /api/v1/labels/<id> for the final status: "purchased" + labelUrl - or skip the polling and subscribe to the shipment.purchased webhook.
Authentication
Every API request must include a Bearer token in the Authorization header:
Authorization: Bearer shp_live_AbCdEf123456789...Tokens have one of two scopes:
read:shipments- GET list/detail, POST quotewrite:shipments- POST create + refund (also implies read access)
Requests with no token return 401 Unauthorized; requests with the wrong scope return 403 Forbidden. Tokens never expire by default but you can set an expiry at creation time + revoke any token from the settings UI.
Security tips
- Treat tokens like passwords. Never commit them.
- Don't put a token in a URL query parameter - URLs leak via referrer + browser history.
- Use the minimum scope necessary. Generate a
read:shipments-only token for monitoring scripts. - Revoke immediately if you suspect a leak.
Endpoints
Base URL: https://shiponline.app/api/v1
All endpoints accept + return JSON. UTF-8. Idempotent writes via the idempotencyKey request field (where supported).
/api/v1/labelsScope: read:shipmentsList your recent shipments, newest first. Cursor-paginated 50 per page.
curl https://shiponline.app/api/v1/labels?cursor=cm123 \
-H "Authorization: Bearer shp_live_..."Response (truncated):
{
"data": [
{
"id": "cm123abc",
"createdAt": "2026-06-21T20:31:00.000Z",
"status": "purchased",
"carrier": "USPS",
"service": "GroundAdvantage",
"serviceCategory": "standard",
"trackingNumber": "9400111899560000000000",
"chargeAmountCents": 695,
"carrierRateCents": 596,
"toName": "Recipient",
"toStreet1": "350 Fifth Avenue",
...
}
],
"pagination": { "hasMore": true, "nextCursor": "cm123abc" }
}/api/v1/labels/{id}Scope: read:shipmentsFetch a single shipment with up to 100 tracking events ordered newest-first.
curl https://shiponline.app/api/v1/labels/cm123abc \
-H "Authorization: Bearer shp_live_..."/api/v1/labels/quoteScope: read:shipmentsFetch live multi-carrier rates without purchasing. Returns the same rate-shape the buy endpoint accepts.
curl -X POST https://shiponline.app/api/v1/labels/quote \
-H "Authorization: Bearer shp_live_..." \
-H "Content-Type: application/json" \
-d '{
"fromAddress": {
"street1": "123 Main St",
"city": "San Francisco",
"state": "CA",
"zip": "94103",
"country": "US"
},
"toAddress": {
"street1": "350 Fifth Avenue",
"city": "New York",
"state": "NY",
"zip": "10118",
"country": "US"
},
"parcel": {
"lengthIn": 10,
"widthIn": 8,
"heightIn": 4,
"weightOz": 16
}
}'/api/v1/labels/createScope: write:shipmentsCreate + buy a label in one call. Returns 202 with status='paying'; poll GET or subscribe to the shipment.purchased webhook for the final state.
Service selection: pick one approach in order of specificity:
carrier+servicetogether for an exact match (e.g."USPS"+"GroundAdvantage")serviceCategoryfor a tier preference (cheapest matching is picked)- Omit both for cheapest across all carriers
For international destinations, the customs field is required.
curl -X POST https://shiponline.app/api/v1/labels/create \
-H "Authorization: Bearer shp_live_..." \
-H "Content-Type: application/json" \
-d '{
"fromAddress": { "street1": "123 Main St", "city": "San Francisco", "state": "CA", "zip": "94103", "country": "US" },
"toAddress": { "street1": "350 Fifth Avenue", "city": "New York", "state": "NY", "zip": "10118", "country": "US" },
"parcel": { "lengthIn": 10, "widthIn": 8, "heightIn": 4, "weightOz": 16 },
"serviceCategory": "standard",
"options": { "signatureConfirmation": true },
"idempotencyKey": "order-1234-attempt-1"
}'/api/v1/labels/{id}/refundScope: write:shipmentsVoid a previously-purchased label. Returns 202 with status='void_requested'. Refund credits your shipping balance, applied automatically to your next label purchase.
curl -X POST https://shiponline.app/api/v1/labels/cm123abc/refund \
-H "Authorization: Bearer shp_live_..."Webhooks
Subscribe to events by registering an HTTPS endpoint in Settings → API tokens → Webhooks. We POST signed JSON payloads to your URL when matching events fire.
Event types
shipment.purchased- label printed successfullyshipment.failed- label purchase failedshipment.refunded- label voidedtracking.update- any carrier scantracking.delivered- terminal delivery eventtracking.exception- carrier-reported exceptionwebhook.test- fires from the "Send test" button
Payload shape
{
"id": "evt_abc123def456",
"type": "shipment.purchased",
"apiVersion": "2026-06-21",
"createdAt": "2026-06-21T20:34:12.001Z",
"data": {
"id": "cm123abc",
"status": "purchased",
"carrier": "USPS",
"service": "GroundAdvantage",
"trackingNumber": "9400111899560000000000",
"labelUrl": "https://shiponline.app/labels/...",
...
}
}Signature verification
Every delivery carries an X-Shiponline-Signature header containing the HMAC-SHA256 of the raw request body using your endpoint secret. Verify in constant time to prevent timing attacks.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhook(rawBody, signatureHeader, secret) {
const expected = createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader);
return a.length === b.length && timingSafeEqual(a, b);
}
// Express handler example
app.post("/webhooks/shiponline", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.header("X-Shiponline-Signature");
if (!sig || !verifyWebhook(req.body.toString("utf8"), sig, process.env.SHIPONLINE_WEBHOOK_SECRET)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString("utf8"));
// handle event.type ...
res.status(200).end();
});Retries
Failed deliveries (network error, 5xx response, 408, 429) retry with exponential backoff: 1 minute → 5 minutes → 25 minutes → 2 hours → 12 hours. After 5 total attempts the delivery is marked failed. Non-retryable 4xx responses (400, 401, 403, 404, etc.) fail immediately - the receiver said no, more attempts won't change that.
Deliveries are deduplicated per endpoint by event id. If our worker retries a dispatch internally, your receiver still sees each logical event exactly once.
Acknowledging deliveries
Respond with any 2xx status code (200, 201, 204) within 10 seconds. Don't do heavy processing in the handler - acknowledge fast + queue the work.
Errors
Standard HTTP status codes. Every error response includes an error field with a human-readable message. Validation errors include an issues array with per-field details.
- 400Bad requestInvalid JSON or missing required fields
- 401UnauthorizedMissing or invalid Bearer token
- 402Payment requiredNo payment method on file (returned from POST /labels/create)
- 403ForbiddenToken doesn't have the required scope
- 404Not foundResource doesn't exist OR exists but belongs to a different account
- 409ConflictResource is in a state where the operation isn't valid (e.g. refunding an already-refunded label)
- 422UnprocessableCarrier rejected the request (no rates available, address invalid, service unavailable)
- 502Bad gatewayCarrier or downstream provider is unreachable; safe to retry
Rate limits
Per-token: 30 requests per minute token-bucket. Exceeding returns HTTP 429 Too Many Requests with a Retry-After header (in seconds). Webhooks are not rate-limited from your side - we control delivery rate.
Need a higher limit? Email support@shiponline.app.
Versioning
The API is versioned via URL prefix: https://shiponline.app/api/v1/... Field additions are non-breaking and ship without a version bump. Field renames, removals, and shape changes ship as a new version (/api/v2/) with at least 6 months of overlap before the prior version retires.
Webhook payload shape changes follow the same rule, with the version exposed via the apiVersion field on every event.
Start building.
Free signup. No extra per-label cost on API calls. Token creation takes 30 seconds.
Create your account