Five Cross-Cutting Design Decisions
Field names and state machines are the easy twenty percent. The other eighty percent is decisions that cut across entities: where retries are made safe, how a label dies, how a package tracks across carriers, how money rolls up, and which ids each layer of your product keeps. Get these five right and the model survives real traffic: splits, multi-leg carriers, void storms, and monthly billing reconciliation.
Where Idempotency Lives
Idempotency is needed at exactly three layers, and each layer needs its own key:
- Order creation: dedupe double-submits from checkout or webhooks.
- Fulfillment creation: prevent double shipping when a WMS retries.
- Label purchase: prevent double billing when a carrier call times out.
The pattern is the same everywhere. The caller generates a UUID and sends it as idempotency_key. The server dedupes on the pair (key, entity_type): the same key can create an order and a label, because they are different entity types. The key expires after a bounded window, typically 24 to 48 hours[^5], after which a new intent needs a new key. On any retry, reuse the same key; the server returns the previously created entity instead of creating a second one.
Putting the key at the wrong layer is where the money leaks. Retry the order POST without a key and you get a double order. Retry the label POST without a key and the carrier charges you twice for one box. Retry a fulfillment without a key and the WMS physically ships the same goods twice. The fulfillment key is the one teams forget most often, because a fulfillment is created inside a larger workflow and looks like an internal step. It is not: it is the step that moves real boxes. For retry strategy and peak-season load testing, see our guide to load-testing a shipping API.
Label Reprint and Void
A label has exactly three states and two of them are boring. purchased is the happy state. reprinted is the same label, same id, same charge, fetched again because the printer jammed; it changes nothing in your data model beyond a status flag and an audit row. voided is the interesting one, because it is terminal and it touches money.
Voiding a label means three things happen, ideally in one transactional API call:
- The carrier is told the label is dead, so it cannot be tendered.
- The label's status moves to
voided, a terminal state.
- A
reversal charge is written that mirrors the original charge amount as negative.
The only path that creates a brand-new label id is void-replace: void the old label, confirm the reversal, then purchase a new one. The ordering matters. Never let two labels with different numbers be valid for the same package at the same time, because a warehouse scanner will happily slap whichever label is newest onto the box, and the carrier will reject the pair. Void first, then purchase.
POST /v1/labels/6f7a8b9c-0d1e-4f2a-3b4c-5d6e7f8091a2/void
{
"reason": "address_correction",
"idempotency_key": "d1e2f3a4-5b6c-4d7e-8f90-1a2b3c4d5e6f"
}
200 OK
{
"label": {
"id": "6f7a8b9c-0d1e-4f2a-3b4c-5d6e7f8091a2",
"status": "voided",
"voided_at": "2026-08-21T16:44:02Z"
},
"reversal": {
"id": "9c0d1e2f-3a4b-4c5d-6e7f-8091a2b3c4d5",
"fulfillment_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"charge_type": "reversal",
"amount": -12.85,
"currency": "USD",
"reason": "voided_label",
"label_id": "6f7a8b9c-0d1e-4f2a-3b4c-5d6e7f8091a2"
}
}
Notice the void call carries its own idempotency key. Voiding is a side-effectful operation too: a retried void request must return the same result, not throw a "label already voided" error that the client cannot distinguish from a real failure.
Multi-Carrier Tracking
Tracking hangs on the package, not on the fulfillment and certainly not on the order. A package can travel several legs, and each leg is its own (carrier, tracking number) pair with its own event stream. The international package in our example has two: DHL for the export leg, USPS for the last mile. If tracking were a single column on the order, this package would already have broken your schema. For keeping one shipment identity across carriers, see our cross-border multi-carrier tracking guide.
- Aggregate events per package, then roll up. Carrier scans append to
package.tracking[segment].events. The fulfillment status derives from the worst state across its packages, and the order status derives from its fulfillments. Roll-up goes bottom-up; never let a carrier webhook write directly to order status.
- Treat (carrier, tracking_number) as the unique key, and dedupe. When one tracking number is polled by multiple systems, or when a regional carrier hands off to a national one that reuses the same number, you get duplicate events. Dedupe by (carrier, tracking_number, event_code, timestamp) before appending, or your delivered-count reports will be quietly wrong.
The composite key matters more than it looks. Tracking numbers are not globally unique. "9400111899..." means nothing by itself; "USPS 9400111899223198765432" means exactly one package. Any join on tracking number alone is a bug waiting for a collision.
Getting Billing Back to the Order and Client
Charges live under the fulfillment, which gives you a deterministic roll-up path: any dollar can be traced fulfillment_id → order_id → customer_id. The east fulfillment's $18.40 label and $6.20 insurance adjustment roll up to order SHOP-1001 and to customer cus_38d2. The west fulfillment's $12.85 charge, its -$12.85 reversal, and its $13.40 replacement roll up the same way. Every dollar has a home, and no dollar has two. For the full methodology, see our shipping reconciliation guide.
The ledger rule is simple and absolute: never mutate an existing charge. If a label is voided, you write a reversal row; you do not delete or edit the original charge. If a carrier surcharge arrives a week later, you write an adjustment row. This is what makes reconciliation possible, because the ledger is append-only and every row carries a reason.
Reconciliation then reduces to one invariant:
order shipping cost = sum(charges) + sum(adjustments) + sum(reversals) across its fulfillments
For our order: east (18.40 + 6.20) plus west (12.85 - 12.85 + 13.40) equals 38.00, and that number must match what the carrier invoices for the two packages. When it does not, the difference points at a specific fulfillment and a specific charge row, which is the whole point of attaching money to the fulfillment rather than to the order or to the customer directly. Client-level billing is a roll-up of these rows, not a separate ledger.
Which IDs the Portal Keeps vs. Which the API Keeps
Every layer of your product should store the ids it actually uses, and the two layers should meet at a documented join key. Confusing the two is how support tickets start with "the tracking number you gave me is not the one on the package".
| Concern |
Portal (human-facing) |
API / system of record |
| Order identity | platform order number, e.g. SHOP-1001 | canonical order UUID |
| Fulfillment | fulfillment display code | fulfillment UUID |
| Package | "Parcel 1 of 2" | package UUID |
| Tracking | display tracking number(s) | canonical tracking value + carrier raw id |
| Label | label number printed on the ticket | label UUID + carrier label id |
| Billing | invoice line items | charge / adjustment / reversal UUIDs |
The portal stores what a human can read, quote, and paste: the platform order number, the tracking number as the carrier prints it, the label number on the ticket. The API and system of record store the canonical UUIDs plus the carrier's raw ids, because those are stable across reprints, platform migrations, and carrier name changes. For the portal-first vs API-first decision, see portal first or API first onboarding.
The join key convention is the contract between the two: canonical_id ↔ external_reference ↔ portal display id, held in one mapping table, never inferred. Do not make the portal's display id the primary key, and do not make the UUID the only way a human finds an order. When someone calls support with "my order 1001", the mapping table is what turns that into a canonical id in one query.