Labels API — Documentation for Clients

REST API for purchasing UPS shipping labels.

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

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

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

  1. Save the order_id as soon as you receive it — it's the only way to re-download the label later.
  2. Don't retry on 4xx errors except 429. They mean your request is wrong, retrying won't help. Fix and re-submit.
  3. Do retry on 503 with exponential backoff (1s, 2s, 5s, 15s).
  4. Validate addresses on your side before sending — typos in ZIP / state are the #1 cause of 422 / 502.
  5. Store the PDF after the first successful download. Re-fetching is supported but not guaranteed forever.
  6. Monitor your balance — call /balance daily; we'll add webhooks for low-balance alerts later.

Support


Changelog