Labels API — Mules Integration Guide
REST API for purchasing UPS shipping labels.
Mules is an internal business branch of OwnerLabels. This document defines the API contract between the Mules application and the OwnerLabels label service.
- Base URL:
https://api.ownerlabels.com - 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.ownerlabels.com/api/v1/balance
# 2. Get the current UPS Ground price (no purchase or charge)
curl -X POST https://api.ownerlabels.com/api/v1/rates \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d @rate.json
# 3. Buy the selected rate (returns order_id + tracking + final price)
curl -X POST https://api.ownerlabels.com/api/v1/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: mules-label-342880" \
-d @order.json
# 4. Download the label PDF
curl -H "Authorization: Bearer YOUR_API_KEY" \
-o label.pdf \
https://api.ownerlabels.com/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
POST /api/v1/ratesreturns the current UPS Ground rate from Pirate Ship plus the OwnerLabels $3.50 markup.- The successful order response repeats the final charged price in
price. - Pricing depends on package weight, dimensions, and destination.
- Rates do not purchase a label and do not change the balance.
- The balance is charged only after the label purchase succeeds and its result is saved.
- Minimum balance: $10.00. You cannot create new orders if your balance is below this threshold. Refunds credit back to your balance and count toward the minimum.
- If your balance is below the $10.00 minimum when you try to buy — the request is rejected with
402 Payment Required(see Errors). - If a successful purchase costs more than the current balance, the label is still delivered and the balance can go negative; top up before high-volume use.
To top up your balance — contact support.
Endpoints
POST /api/v1/rates
Returns the current UPS Ground price including the OwnerLabels $3.50 markup.
It does not purchase a label and does not change the client balance. The request
uses the same ship_from, ship_to, and package objects as order creation.
Request body:
{
"ship_from": {
"name": "John Sender",
"company": "Mules",
"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"
}
Response 200:
{
"rate_id": "psq_...",
"carrier": "ups",
"service": "Ground",
"price": 12.03,
"currency": "USD"
}
Pass the returned opaque rate_id in POST /api/v1/orders so the confirmed quote stays linked to the intended purchase.
If it is rejected or has expired, request a fresh rate and let the user confirm
the new displayed price before retrying the purchase.
Treat rate_id as an opaque short-lived value: do not modify it and do not reuse
it for a different address, parcel, or user-confirmed purchase.
GET /api/v1/healthz
Liveness probe. No authentication required.
curl https://api.ownerlabels.com/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.ownerlabels.com/api/v1/balance
Response 200:
{
"client": "Acme Inc",
"balance": 88.98,
"currency": "USD"
}
POST /api/v1/orders
Creates an order and immediately purchases the selected UPS Ground label.
Returns the order id, tracking number, and final price. Mules must call Rates
first and pass its rate_id in this request.
curl -X POST https://api.ownerlabels.com/api/v1/orders \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: mules-label-342880" \
-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",
"external_order_id": "342880",
"shipping_id": "SHIP-12345",
"rate_id": "rate_..."
}
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). Mules currently supports only UPS Ground. |
carrier |
no | string | Defaults to ups; only ups supported today |
external_order_id |
no | string | Client/integration order id. Used as the main idempotency fallback if Idempotency-Key header is missing. |
shipping_id |
no | string | Mules Shipping ID / concrete label id, stored for lookup/debugging. Used as idempotency fallback if no order id is provided. |
rate_id |
yes for Mules | string | rate_id returned by the immediately preceding Rates request. It remains optional in the API only for backward compatibility. |
Integration note: Mules is an internal OwnerLabels business branch. There is no
provider_idto send; the integration is identified by its API key and base URL.Duplicate protection: safest option is to send a unique
Idempotency-Keyper label purchase. If the header is missing, OwnerLabels dedupes retries byexternal_order_id; if no order id is provided, it falls back toshipping_id.Total weight =
weight_lbs * 16 + weight_oz(in ounces). Must be ≥ 1 oz.
Response 201 Created:
{
"order_id": 123,
"status": "purchased",
"tracking_number": "1Z19D2C70325572916",
"tracking_url": "https://www.ups.com/track?tracknum=1Z19D2C70325572916",
"external_order_id": "342880",
"shipping_id": "SHIP-12345",
"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.
External IDs are echoed back in create/status responses so Mules can map:
external_order_id → shipping_id → order_id → tracking_number → label_url
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.ownerlabels.com/api/v1/orders/123
Response 200:
{
"order_id": 123,
"status": "purchased",
"tracking_number": "1Z19D2C70325572916",
"tracking_url": "https://www.ups.com/track?tracknum=1Z19D2C70325572916",
"external_order_id": "342880",
"shipping_id": "SHIP-12345",
"price": 12.34,
"label_url": "/api/v1/orders/123/label",
"error": null
}
Lookup by external IDs:
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.ownerlabels.com/api/v1/orders/by-external/342880
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.ownerlabels.com/api/v1/orders/by-shipping/SHIP-12345
Each endpoint returns the same OrderResponse shape as GET /api/v1/orders/{order_id}.
Order statuses:
| Status | Meaning |
|---|---|
pending |
Order created but the label purchase hasn't completed (transient — rare) |
processing |
Purchase outcome needs recovery/reconciliation. Do not start a replacement purchase with new identifiers; contact support. |
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.ownerlabels.com/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".
POST /api/v1/orders/{order_id}/refund
Requests a refund for a previously-purchased label. The full price you paid is credited back to your balance (including our fee). Refunds are typically instant for UPS — your balance reflects the credit immediately.
curl -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
https://api.ownerlabels.com/api/v1/orders/123/refund
Response (instant refund — UPS):
{
"order_id": 123,
"refund_status": "refunded",
"refund_amount": 12.84,
"requested_at": 1781205198,
"refunded_at": 1781205198,
"estimated_refund_date": null,
"message": "Refund processed instantly. Balance credited."
}
Response (pending — non-instant carriers):
{
"order_id": 123,
"refund_status": "requested",
"refund_amount": null,
"requested_at": 1781205198,
"refunded_at": null,
"estimated_refund_date": "2026-06-25T19:12:17+00:00",
"message": "Refund requested. Carrier will confirm within ~14 days. Balance will be credited automatically."
}
Rules:
- Refund can be requested only after 10 days from the original purchase date. Before that the endpoint returns
409with the exact date when refund becomes available. - The label must not have been scanned/used by the carrier. If the carrier rejects the refund (label already in transit, etc.), you'll get
409with the carrier's explanation. - A refund can only be requested once per order. Calling
/refunda second time returns the current state (no duplicate processing, no double-credit). - Once
refund_status == "refunded"the credit is on your balance — verify withGET /balance. - For pending refunds (rare on UPS, common on other carriers), poll
GET /api/v1/orders/{id}untilrefund_statusbecomesrefundedordenied. The system polls the carrier every 15 minutes and credits your balance automatically when confirmed.
Checking refund status later — the existing GET /api/v1/orders/{id} response now includes refund fields:
{
"order_id": 123,
"status": "purchased",
"tracking_number": "1Z2X8C700323237900",
"price": 12.84,
...
"refund_status": "refunded",
"refund_amount": 12.84,
"refund_requested_at": 1781205198,
"refunded_at": 1781205198
}
refund_status is one of: none (never requested), requested (waiting on carrier), refunded (done — balance credited), denied (carrier rejected — see error field or contact support).
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 below the $10.00 minimum required to create new orders | 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 |
Temporary purchase throttle/rate-limit, if enabled | Slow down and retry after the Retry-After header |
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 |
504 Gateway Timeout |
The request timed out before a purchase could start | Check the order lookup first; retry only with the same idempotency identifiers |
Examples:
// 401
{ "detail": "Invalid API key" }
// 402
{ "detail": "Insufficient balance. Minimum $10.00 required to create an order, current balance is $5.40. Please top up to continue." }
// 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
- Always use the two-step flow: Rates → user confirms the displayed price → Orders with the returned
rate_id. - Send your unique order id in
external_order_id. This prevents duplicate purchases if a request is retried. - Send one stable
Idempotency-Keyper intended label purchase. Reuse the same key when retrying that purchase; never generate a new key for a transport retry. - Save
order_idas soon as you receive it — use it to re-download the label later. - Don't retry on 4xx errors except
429. They usually mean the request must be fixed first. - After a timeout or 5xx, query by
external_order_idorshipping_idfirst. If no order exists, retry with exponential backoff (1s, 2s, 5s, 15s) and the same idempotency identifiers. Never generate new identifiers for a transport retry. - 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-09-01 — v0.3.0 — Added no-purchase UPS Ground Rates and purchase by selected
rate_id; clarified the Mules two-step flow and idempotent retries. - 2026-06-11 — v0.2.1 — Minimum balance of $10 required to create orders.
- 2026-06-11 — v0.2.0 — Added refunds. POST
/api/v1/orders/{id}/refund(instant on UPS, 10-day window). GET/orders/{id}now includes refund fields. - 2026-05-29 — v0.1.0 — Initial release. UPS Ground only, single carrier, prepaid balance.