Skip to content

Buying labels safely

Buying a label spends money from your wallet. The API splits the work in two calls so you can look before you spend.

  1. POST /shipments/quote rates the package and saves it as a shipment. It costs nothing.
  2. POST /shipments/{id}/buy buys one of the returned rates.

A quote needs the ids of a saved ship-from address (GET /ship-from), a destination, and a parcel. See the API reference for every field. The response includes:

Field Meaning
shipment_id The saved shipment. You pass it to the buy call.
rates The prices your organization pays. Each has id, carrier, service, price_cents, delivery_days, guaranteed and badges.
suggested_rate_id and suggested_by_rule Set when one of your shipping rules chose a rate.
applied_rules Which rules ran and what they did.
partial true when some carriers did not answer.
expires_in_seconds How long the rates can be bought.

Rules that remove rates act on the quote itself, so a rate a rule removed is not in the list and cannot be bought.

If the quote request has "verify_address": true (the default) and your account is set to confirm address changes, the response is 200 with "status": "needs_address_confirmation", the address you entered and a suggestion. Show the suggestion to a person, then quote again with the address you want. Pass "verify_address": false to skip the check.

If you buy after the rates expired, the call fails with 409 and the code conflict, with details.expired set to true. Quote again and use the new rate.

When partial is true the cheapest carrier may be missing. For an unattended system it is usually right to quote again after a short wait. For a person, show the rates you have and say that the list may be incomplete.

The buy call requires an Idempotency-Key header of 8 to 200 characters. Without it the call fails with 422 validation.

Terminal window
curl https://shipjay.com/api/v1/shipments/$SHIPMENT_ID/buy \
-H "Authorization: Bearer $SHIPJAY_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001-label-1" \
-d '{ "rate_id": "r_5f3a91" }'

How the key behaves:

  • The same key with the same request returns the first result and adds the header Idempotent-Replayed: true. Nothing is bought a second time.
  • The same key with a different request fails with 422 validation.
  • The same key while the first request is still running fails with 409 conflict. Wait and send it again.
  • A server error (5xx) forgets the key, so you can retry with it.
  • Keys are scoped to the API key that sent them.

Derive the key from something stable in your system, such as an order id plus a counter, not from a random value generated on each retry. A random key per attempt defeats the protection. Buying a shipment that is already purchased also fails with 409, and the response body has "duplicate": true when it repeats a purchase made with the same key.

If a buy times out and you do not know the outcome, repeat the call with the same key. If you lost the key, read the shipment (GET /shipments/{id}) and check its status before you do anything else.

Three limits can stop a purchase, all with 402:

  • The key's per label limit (spend_limit).
  • The key's per day limit (spend_limit).
  • Your account's daily limit (spend_limit) and your wallet balance (insufficient_funds).

Set a per label and per day limit on every key that can buy. Then a bug or a leaked key can lose at most one day of the limit.

  • GET /shipments/{id}/label downloads the label file.
  • GET /shipments/{id}/tracking returns the status and scan history.
  • Subscribe to shipment.purchased and tracking.updated to avoid polling. See Webhooks.

POST /shipments/{id}/refund asks the carrier to void an unused label. It needs the refunds:write permission. The response has a refund_status:

Status Meaning
submitted The carrier has the request. The wallet is credited when it approves, which can take days.
refunded Approved and credited already.
rejected The carrier refused.
not_applicable The label cannot be refunded.

Only purchased labels can be refunded, and asking twice fails with 409. You get shipment.refund_requested, then either shipment.refunded or shipment.refund_rejected events.

© 2026 Ship Jay. All rights reserved. Created & Developed by WCWS Internet