GoatLabels GoatLabels

Blog

Build Shipping Into Your App in a Weekend

A practical REST tutorial: quote four carriers with one request, buy a label, store the artifacts, and let signed webhooks keep your app in sync.

A minimal shipping integration is four moves: send an address and a parcel to a quote endpoint and get USPS, UPS, FedEx, and DHL prices back in one response; buy the quote you want; store the label file and tracking number your app gets back; and stand up one webhook endpoint so shipment updates push to you instead of you polling for them. On GoatLabels the REST API comes with unlimited calls on every plan and pay-per-label pricing, so the weekend version of this project costs you the price of one test label — and this guide walks through all four moves.

What does a minimal shipping integration actually need?

Strip away the enterprise ceremony and a shipping feature is a short contract between your app and the API:

  • Your app supplies a destination address, a parcel (weight and dimensions), and — for international — customs items.
  • The API supplies validated addresses, four carriers' quotes with one all-in price each, purchased labels, tracking numbers, and event pushes when anything changes.
  • Your database keeps its own shipment row per order: the shipment ID, chosen rate, cost, label URL, tracking number, and status.

That's it. You don't need a queue, an orders-sync engine, or a carrier abstraction layer of your own — the API is the abstraction layer. Authentication is an API key in a header; there are no per-request fees and no call quotas to engineer around, which is a deliberate contrast to per-label-fee APIs (the comparison lives at EasyPost alternative with a free API).

Here's the whole weekend, milestone by milestone:

MilestoneWhat you callWhat your app stores
1. Quote a shipmentPOST a quote request with address + parcelNothing yet — quotes are free and disposable
2. Buy a labelPOST a purchase for the chosen rateShipment ID, carrier, service, all-in cost, label URL, tracking number
3. Serve the artifactsGET the shipment by ID when you need to re-fetchLabel PDF path for reprints; tracking number surfaced to the buyer
4. Receive webhooksYour endpoint; verify the signature on each eventShipment status transitions; adjustment records with their wallet delta
5. Handle adjustmentsshipment.adjustment.created event (also queryable via API)The adjustment record linked to its shipment, for true per-order cost

Exact request and response schemas are in the API docs — this guide is the map, not the reference.

How do you quote four carriers with one request?

The quote endpoint is where a shipping integration stops being intimidating. One request — recipient address, parcel weight, dimensions — returns rate objects for USPS, UPS, FedEx, and DHL together, each carrying a rate ID, the service name, an estimated transit time, and one all-in price. No per-carrier accounts to register, no four integrations wearing a trench coat.

Two behaviors at quote time do real work for you:

  • Address validation is built into the quote. The destination is checked for deliverability and badged residential or commercial before you see prices. Your checkout can surface a bad address while the customer is still on the page — the cheapest possible moment to fix it — instead of discovering it as a returned package.
  • Extras fold into the price. Insurance is a per-shipment toggle, and a signature requirement is set per shipment; include them in the quote request and the fee shows up inside the quoted price, not as a surprise line on a bill later.

Quotes cost nothing and there's no call quota, so use them liberally: quote at checkout to show live shipping prices, re-quote in your admin view before fulfillment, quote speculatively to build a "cheapest option" default. Your only decision is selection logic — cheapest rate always, or cheapest within a transit-time ceiling. Persist nothing at this stage; a quote you didn't buy is garbage-collectible.

For international destinations, add customs items to the request — description, declared value, origin country, and HS code per item. You supply the HS codes (the API doesn't classify goods for you), and saved customs templates mean your app can attach a stored declaration for repeat products instead of rebuilding it per order.

How do you buy a label and store the artifacts?

Purchase is a second call: take the rate ID your logic (or your user) selected and post the buy. The all-in price is drawn from your prepaid wallet — the same wallet you fund by card, Apple Pay, Google Pay, ACH, or crypto — and the response hands back everything your app should keep:

  • The shipment ID — your foreign key for everything that follows: re-fetches, webhook correlation, adjustments.
  • The label file — store your copy or store the URL, but either way make reprinting a first-class feature. "The label didn't print" is the most common support ticket a shipping feature generates, and re-download beats re-buy.
  • The tracking number — write it to the order and surface it to the buyer. Every shipment also gets a branded tracking page and tracking emails out of the box, so "where's my order" has an answer you didn't build.
  • The cost actually charged — record it per shipment now and per-order shipping economics become a query instead of a spreadsheet project.

Model the shipment as its own table keyed by shipment ID, linked to your order — one order can legitimately produce several shipments (split orders, reships, return labels). Treat GoatLabels as the source of truth for shipment state and your database as a synchronized read model; when in doubt, GET the shipment by ID and reconcile.

How do signed webhooks keep your app in sync?

Polling every shipment for status is the pattern that turns weekend projects into cron-job zoos. Webhooks invert it: you expose one HTTPS endpoint, and events arrive as shipments move — status transitions your UI can mirror, and the operationally important shipment.adjustment.created when a carrier reweighs or remeasures a package after acceptance and the cost is corrected.

Three rules make the endpoint production-grade in an afternoon:

  1. Verify the signature on every event. Every webhook is signed; check the signature before trusting the payload, and reject what doesn't verify. A tracking-update endpoint that skips this is an unauthenticated write path into your order system.
  2. Acknowledge fast, process async. Return a success response immediately and hand the payload to your own job queue. Slow handlers get retried, and retries mean you must also...
  3. Be idempotent. Key processing on the event ID so a redelivered event is a no-op, not a duplicate status change.

Handle adjustments as data, not surprises: adjustments arrive as records — via the webhook and queryable through the API — each with its delta. Link the record to its shipment and roll it into that order's true cost. Note the wallet nuance for your billing assumptions: an adjustment draws on wallet balance, and if the balance doesn't cover it, the card on file is charged. Event-by-event handler design, retry semantics, and which events a small app can safely ignore get a full treatment in shipping webhooks explained.

What does testing look like before real money moves?

The quote flow is free to exercise as hard as you like — no purchase, no cost, unlimited calls. So the bulk of your integration test suite (address handling, validation failures, rate selection, extras folding into prices) runs without money moving at all. When you're ready to prove the purchase path end to end:

  1. Buy one real, cheap label to your own address — the cheapest USPS quote to yourself. Assert your app stored the shipment ID, label, cost, and tracking number correctly.
  2. Print it and eyeball it. A label PDF that renders wrong at 4×6 is a bug no JSON assertion catches.
  3. Exercise the webhook path by watching the shipment's events hit your endpoint: signature verifies, handler acknowledges fast, replayed event no-ops.
  4. Void the label you don't ship. Voiding an unused label returns its cost to your wallet as spendable credit — carriers set an outside window for unused-label refunds, so void promptly rather than letting test labels age. Your total test spend rounds to zero.

Also test your failure UX on purpose: quote an address with a nonexistent street number and confirm your checkout surfaces the validation failure gracefully. The unhappy path is half the integration.

What will this cost at your volume?

The pleasant answer: the API itself costs nothing at any volume. Unlimited API calls on every plan, no monthly minimum, no per-label API fee — you pre-fund the wallet (card, Apple Pay, Google Pay, ACH, or crypto) and pay the all-in label price when you buy. Ten labels a month costs ten labels; zero labels costs zero. That makes shipping a feature you can ship before you know your volume, which is precisely the position a weekend-project app is in.

When volume arrives, the optional $40/mo Pro plan buys cheaper rates and unlimited shipments — a straightforward break-even you can compute from your own average savings per label. Until then, the free tier is the whole product: four carriers, Billy, the API, webhooks, branded tracking. Details at pricing.

FAQ

Do I need my own carrier accounts with USPS, UPS, FedEx, or DHL? No. One API key gets you quotes and labels across all four carriers with one all-in price each — there's no per-carrier registration or rate negotiation on your side.

Is the API rate-limited or metered on the free plan? API calls are unlimited on every plan, and quoting is free. You pay only the all-in price of labels you actually purchase, drawn from your prepaid wallet.

How does my app find out about carrier billing adjustments? Subscribe to shipment.adjustment.created. Each adjustment arrives as a record with its delta, linked to the shipment, and is also queryable via the API. Note that if an adjustment exceeds your wallet balance, the card on file is charged for the difference.

Can I test purchases without spending real money? Quoting is free and covers most integration testing. For the purchase path, buy one cheap label to yourself and void it unused — the cost returns to your wallet as spendable credit, so a full end-to-end test costs effectively nothing.

What must my webhook endpoint do to be safe? Three things: verify the signature on every event before trusting it, respond quickly and process asynchronously, and deduplicate by event ID so retries don't double-apply. That's the entire hardening checklist for a small app.