Labels API — Documentation for Clients
REST API for purchasing UPS shipping labels.
- Base URL:
https://api.easyship.lol(provided per client) - Authentication: API Key (Bearer token in
Authorizationheader) - Format: JSON request/response, UTF-8
- Versioning: all endpoints under
/api/v1/
Important: an API key is issued personally for each client. Keep it secret — it has direct access to your prepaid balance. If the key is leaked, contact support immediately to regenerate it.
Quick Start
# 1. Check balance
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.easyship.lol/api/v1/balance
# 2. Buy a label (returns order_id + tracking + price)
curl -X POST https://api.easyship.lol/api/v1/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d @order.json
# 3. Download the label PDF
curl -H "Authorization: Bearer YOUR_API_KEY" \
-o label.pdf \
https://api.easyship.lol/api/v1/orders/123/label
Authentication
Every request must include the header:
Authorization: Bearer <YOUR_API_KEY>
Keys look like: lk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
On 401 Unauthorized — the key is invalid, deactivated, or missing.
Pricing
- The final price for each label is returned in the
pricefield of the order response. There is no separate breakdown — only the amount you will be charged. - Pricing depends on package weight, dimensions, and destination.
- Each label is charged from your prepaid balance the moment it's purchased.
- If your balance is insufficient when you try to buy — the request is rejected with
402 Payment Required(see Errors).
To top up your balance — contact support.
Endpoints
GET /api/v1/healthz
Liveness probe. No authentication required.
curl https://api.easyship.lol/api/v1/healthz
Response 200:
{ "ok": true, "service": "labels-api", "version": "0.1.0" }
GET /api/v1/balance
Returns the current balance of the authenticated client.
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.easyship.lol/api/v1/balance
Response 200:
{
"client": "Acme Inc",
"balance": 88.98,
"currency": "USD"
}
POST /api/v1/orders
Creates an order and immediately purchases the label from UPS. Returns the order id, tracking number, and final price.
curl -X POST https://api.easyship.lol/api/v1/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d @order.json
Request body:
{
"ship_from": {
"name": "John Sender",
"company": "Acme Inc",
"address1": "1600 Amphitheatre Pkwy",
"address2": "Suite 200",
"city": "Mountain View",
"state": "CA",
"zip": "94043",
"country": "US",
"phone": "5555555555"
},
"ship_to": {
"name": "Jane Receiver",
"address1": "350 Fifth Avenue",
"city": "New York",
"state": "NY",
"zip": "10118",
"country": "US",
"phone": "5555555555"
},
"package": {
"weight_lbs": 1.0,
"weight_oz": 0,
"length": 6,
"width": 6,
"height": 6
},
"service": "Ground",
"carrier": "ups"
}
Field reference:
| Field | Required | Type | Notes |
|---|---|---|---|
ship_from.name |
yes | string | Sender full name, 1–120 chars |
ship_from.company |
no | string | Optional company name |
ship_from.address1 |
yes | string | Street address |
ship_from.address2 |
no | string | Apt / Suite / Unit |
ship_from.city |
yes | string | |
ship_from.state |
yes | string | 2-letter US state code (e.g. FL) |
ship_from.zip |
yes | string | US ZIP, 5 or 9 digits |
ship_from.country |
no | string | Defaults to US, 2-letter ISO |
ship_from.phone |
no | string | Sender phone |
ship_to.* |
same | Same shape as ship_from |
|
package.weight_lbs |
yes | number | Pounds (use 0 if ounces only) |
package.weight_oz |
no | number | Ounces, added to pounds |
package.length |
yes | number | Inches, max 108 |
package.width |
yes | number | Inches, max 108 |
package.height |
yes | number | Inches, max 108 |
service |
no | string | Ground (default), 2nd Day Air, 3 Day Select, Next Day Air, Next Day Air Saver |
carrier |
no | string | Defaults to ups; only ups supported today |
Total weight =
weight_lbs * 16 + weight_oz(in ounces). Must be ≥ 1 oz.
Response 201 Created:
{
"order_id": 123,
"status": "purchased",
"tracking_code": "1Z19D2C70325572916",
"tracking_url": "https://www.ups.com/track?tracknum=1Z19D2C70325572916",
"price": 12.34,
"label_url": "/api/v1/orders/123/label",
"error": null
}
The label is ready immediately — fetch it with GET /api/v1/orders/123/label.
GET /api/v1/orders/{order_id}
Returns the current status of an order. Useful for re-checking an old order or polling if you lost the original response.
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.easyship.lol/api/v1/orders/123
Response 200:
{
"order_id": 123,
"status": "purchased",
"tracking_code": "1Z19D2C70325572916",
"tracking_url": "https://www.ups.com/track?tracknum=1Z19D2C70325572916",
"price": 12.34,
"label_url": "/api/v1/orders/123/label",
"error": null
}
Order statuses:
| Status | Meaning |
|---|---|
pending |
Order created but the label purchase hasn't completed (transient — rare) |
purchased |
Label is ready to download |
failed |
Purchase failed; check the error field. No charge has been applied. |
GET /api/v1/orders/{order_id}/label
Downloads the label as a PDF file (Content-Type: application/pdf).
curl -H "Authorization: Bearer YOUR_API_KEY" \
-o label_123.pdf \
https://api.easyship.lol/api/v1/orders/123/label
- Response body is the raw PDF binary.
- Browser-friendly:
Content-Disposition: attachment; filename=label_<tracking>.pdf. - Only returns the file when
status == "purchased".
Errors
All errors are returned as JSON with HTTP status codes following standard REST conventions:
{ "detail": "Human-readable error message" }
| HTTP | Meaning | What to do |
|---|---|---|
400 Bad Request |
Malformed JSON or missing fields | Check the request schema |
401 Unauthorized |
Missing or invalid API key | Verify the Authorization header |
402 Payment Required |
Your balance is too low for this label | Top up your balance |
404 Not Found |
Order id doesn't exist (or doesn't belong to your account) | Check the id |
409 Conflict |
Label not yet ready (status ≠ purchased) |
Poll the order endpoint |
410 Gone |
Label file is missing on our storage | Contact support |
422 Unprocessable Entity |
Validation failed — bad ZIP, weight too low, invalid service, etc. | Read detail, fix and retry |
429 Too Many Requests |
Rate-limit (not enforced yet, future) | Slow down |
502 Bad Gateway |
Carrier (UPS) refused the shipment for a reason we couldn't categorize | Read detail; usually an address issue. Retry won't help — fix and re-create the order. |
503 Service Unavailable |
We can't reach the carrier right now (auth/network outage on our side) | Retry in 1–2 minutes |
Examples:
// 401
{ "detail": "Invalid API key" }
// 402
{ "detail": "Insufficient balance: requires $12.34, you have $5.40" }
// 422
{ "detail": "Service 'ups Overnight' not available for this shipment" }
// 422
{ "detail": "Package weight too small (need ≥1 oz)" }
// 503
{ "detail": "Upstream provider unavailable. Try again later." }
Best Practices
- Save the
order_idas soon as you receive it — it's the only way to re-download the label later. - Don't retry on 4xx errors except
429. They mean your request is wrong, retrying won't help. Fix and re-submit. - Do retry on 503 with exponential backoff (1s, 2s, 5s, 15s).
- Validate addresses on your side before sending — typos in ZIP / state are the #1 cause of 422 / 502.
- Store the PDF after the first successful download. Re-fetching is supported but not guaranteed forever.
- Monitor your balance — call
/balancedaily; we'll add webhooks for low-balance alerts later.
Support
- Issues, key rotation, top-ups: contact your account manager
- Status page: TBD
Changelog
- 2026-05-29 — v0.1.0 — Initial release. UPS Ground only, single carrier, prepaid balance.