These value objects are embedded in resources. They have no ID of their own unless noted. How they fit together on a shipment:
Each object holds one kind of truth. Who is involved lives in contacts, and who pays is bill_to plus billing. What is in the box lives in items, inside the parcel that physically holds them. What was sold lives in the order. What is declared to customs lives in the customs declaration. Which product, with which features, lives in carrier_configs: the service plus standardized and carrier-specific features, per carrier. Earlier drafts, and most carrier APIs, fold several of these into a single “customs” object. That object then breaks the first time a parcel is domestic but still has dangerous goods, or an order splits across two handling_units. The same structure as a class diagram, with the cardinality of each slot:

1. Contact

A party to a shipment, with its address nested. This follows Stripe’s shipping and billing_details, and the shipFrom / shipTo / billTo convention common in carrier APIs. A shipment has four party slots, all of type Contact:

1.1 Bill-to and billing

bill_to says who the carrier invoices. billing says how, meaning which carrier account is charged.
Rules:
  • Carriers validate third-party and collect billing against the account’s registered country and postal code. The implementer sends bill_to.address.country and postal_code with the account number, so bill_to.address MUST match the address the carrier has on file for that account.
  • Billing affects price. Recipient and third-party accounts are rated on their own contracts, or not at all. So bill_to and billing belong to the rate request, and a rate is only valid for the billing it was quoted with. Carriers that cannot quote another party’s account return list rates, with rate.pricing: "list".
  • Returns. On a return (returns.is_return: true), ship_from is the customer, but sender billing still means the connection’s account, which is the merchant. bill_to then defaults to the outbound shipment’s ship_from, not the customer.
  • bill_to is not the same as order.buyer. The buyer is who the merchant invoiced for the goods. bill_to is who the carrier invoices for the transport. On a commercial invoice, the “sold to” party is order.buyer, falling back to ship_to.
  • Duties and taxes have their own billing in customs_declaration.duties (§5), because a shipment can be freight-prepaid by the sender and duties-collect from the recipient.
How billing.paid_by decides the billed account and the default bill_to:

Address

Street number rules.
  • When a carrier wants it separate (DHL Parcel DE, PostNL, DPD, Correios): the implementer uses street_number if it is given. Otherwise it MAY parse it out of line1. If parsing is ambiguous and the carrier requires the field, the request fails with 400 parameter_missing, param: "ship_to.address.street_number", rather than guessing.
  • When a carrier wants one street line (most US, UK and FR carriers): the implementer joins the two in the destination country’s order. That is number first in US, GB, FR, CA and AU (10 Downing St), and number last in DE, NL, BE, AT, CH, PL and BR (Hauptstraße 5).
  • Clients never put the number in both fields.
The same rules as a decision flow: Why nest the address. A contact without an address is useful (an EU responsible person, a broker’s phone line), and an address without a contact is useful (a pickup location, a manifest origin). Nesting lets each exist alone, and lets address be reused by itself on pickups and manifests.

2. Handling unit

Anything that gets its own label: a parcel, an envelope, a pallet, a crate. The spec uses one object for all of them. The freight term “handling unit” covers both worlds, and a shipment can mix them, such as two pallets plus a loose carton.
A pallet:
Implementers MUST reject a unit whose items’ summed weight (unit_gross_weight, falling back to unit_net_weight, × quantity) exceeds the unit’s weight (parameter_invalid, param: "handling_units[0].weight"). They MUST NOT reject the opposite case, because packaging has weight. Services declare which unit types they accept (chapter 14 §1, limits.handling_unit_types). A rate request mixing types that no service accepts returns no rates for that carrier, with errors[].code = "handling_unit_type_unsupported".

3. Item

One line of goods inside a parcel. The fields follow the richer commodity models of cross-border carriers, with HS codes split by direction and dangerous goods moved here, so that they sit on the thing that is dangerous.
Net and gross weight. Commercial invoices from DHL Express and FedEx, EU export declarations, and several import regimes (CN, BR, IN) ask for both per line. Rules:
  • Declarations use unit_net_weight × quantity as the line’s net weight.
  • Where a carrier or declaration needs line gross weight and unit_gross_weight is absent, implementers use the net weight and report it in messages[] (gross_weight_derived).
  • Neither field replaces the parcel weight, which is the gross weight of the packed shipping carton, and the figure carriers bill on.
Why two HS codes. Only the first 6 digits are harmonized across countries. The digits after that are national, so the same shirt is 6109.10.0012 on a US export filing and 6109100010 on the UK import tariff. Carriers ask for different ones: export declarations (AES/EEI, the EU export system) want the export code, and DDP pricing and import clearance want the import code. One hs_code field forces every integrator to guess which one the carrier meant. Rules:
  • Supply at least one of the two codes for customs. If only one is given, implementers MAY use its first 6 digits where the other is needed, and MUST report that in messages[] (hs_code_derived).
  • The implementer MUST NOT invent a code. Classification is a separate concern, and an implementer offering it exposes it as its own capability (classification) that returns suggestions, never silently filling these fields.
Which code goes where, and what happens when only one is supplied:

ItemCompliance

All fields are optional. control_type is one of dual_use, military, sanctions, agricultural, pharmaceutical, cites, other.

DangerousGoods

Lives on the item, because a lithium battery is a property of the power bank, not of the shipment.
If any item has dangerous_goods, the implementer MUST only rate services whose carrier declares the dangerous_goods capability and accepts that class. The implementer MUST NOT silently drop the field, because the failure mode is a fire on an aircraft.

4. Order

Optional. The commercial transaction behind the shipment, which is what was sold and for how much. It is an embedded value, not a resource, and order management stays in the OMS. Customs needs it for invoice data, DDP and VAT calculation (freight is part of the customs value in the EU and UK), and documents.
An order may span several shipments (a split shipment). Send the same order.id on each one, and the implementer MUST NOT assume order.total equals the sum of one shipment’s items.

5. Customs declaration

Required when origin and destination are in different customs territories. The implementer decides that, including intra-EU, GB↔NI, US↔PR, and the Canary Islands. The what is declared from handling_units[].items[], so this object holds only the declaration itself.
Corridor-specific fields with no canonical home (Brazil’s CPF purpose, India’s IEC) go in the carrier config, where the carrier’s schema validates them. The declaration has no free-form options bag. Why at rating time. Cross-border carriers price landed cost (duty plus tax plus fees) into the quote. Because the order, the items and the declaration are all accepted at rating time, a DDP rate can be exact rather than estimated.

6. Carrier configs: products and features

carrier_configs is the shipment’s product and feature configuration. It is the rethought successor of three things integrators already know: It is what a shipping method configuration holds in an ERP or WMS (the carrier, the product, and the additional services enabled for it), carried on the shipment.
Here every carrier is asked for an adult signature and $250 cover. DHL Express overrides the signature level, FedEx adds Saturday delivery (standardized) and Priority Alert (FedEx-specific), and UPS is rated with only the * features.

Semantics

  • Keys select carriers. Each carrier key adds that carrier to the rating set, and {} means “this carrier with its defaults”. * selects no carrier: it holds standardized features applied to every rated carrier. If no carrier key is present, every active connection is rated, with * applied.
  • service selects the product. In a rate request it narrows that carrier to one service, and omitting it returns every eligible service. A purchase without a rate needs exactly one carrier key with a service.
  • connection selects the account when the account has several for the carrier.
  • Standardized features (§6.1) have one name, one type and one meaning across all carriers. The implementer translates each one into the carrier’s own product codes, flags and service options, exactly as Karrio’s connectors map options today. They may appear in * or in a carrier entry.
  • Carrier-specific features cover everything the carrier offers that the spec does not standardize. Their names and types come from the carrier’s shipment_config_schema (generated from its catalog, chapter 14 §3). They are allowed only inside that carrier’s entry, never in *. Standardized names are reserved, and a carrier-specific feature MUST NOT reuse one with a different meaning.
  • Precedence, from lowest to highest: the carrier’s defaults, then the connection’s config.shipment_defaults, then *, then the carrier entry.
  • Unknown keys are rejected (400 parameter_unknown, param: "carrier_configs.fedex.signture").

Honoring standardized features

A standardized feature is a requirement, not a hint. The implementer MUST NOT silently drop one.
  • Rating. A service that cannot provide a requested standardized feature is excluded from rates[]. The exclusion is listed in rate_quote.errors[] with code: "option_unsupported" and param naming the feature. A shipper asking for adult signature never sees a rate that cannot deliver it.
  • Purchase. It fails with 422 carrier_error / option_unsupported.
  • Transparency. Every rate states how each standardized feature was fulfilled at that carrier, so the mapping is visible rather than implied:
rate.carrier_configs is the effective configuration (one key) that a purchase of the rate buys. rate.features explains, per standardized feature, the carrier’s native code and its price.

Flow

One shape everywhere

Rates, purchased shipments, pickups, pickup schedules and manifests all use carrier_configs. Where only one carrier is involved, the map has one key and no *. The schemas are shipment_config_schema, pickup_config_schema and manifest_config_schema. There is no singular carrier_config field anywhere in the spec.

6.1 Standardized features

The normative registry. The set is append-only, and a feature graduates into it when two or more carriers offer the same semantics. Each carrier declares which features it supports in carriers/{id}.standardized_features, per service in its catalog. Why one object. Splitting the product (service) from its features (options) forced clients to know which features each product allows, and to re-send both on every call. Putting them together under the carrier they belong to makes the request read like a shipping method configuration. Standardized features keep cross-carrier intent simple. Carrier-specific features keep every carrier’s full surface. rate.features makes the translation between them auditable.

7. Service

Read-only on rates and shipments:
  • code is the implementer’s normalized service code ({carrier}_{service}). It is stable across versions, and it is what carrier_configs.*.service takes.
  • carrier_code is the raw carrier product code.
  • level is one of same_day, overnight, express, standard, economy, freight, other.

8. Charge

kind values: base, fuel_surcharge, residential_surcharge, remote_area_surcharge, peak_surcharge, oversize_surcharge, dangerous_goods_surcharge, option (with an option key naming the carrier-config key), insurance, duty, tax, customs_fee, pickup_fee, discount (negative), adjustment, other.

9. Delivery estimate

10. Document (label, invoice, manifest PDF)

  • type: label, return_label, commercial_invoice, proforma_invoice, certificate_of_origin, customs_declaration (CN22/CN23), packing_slip, pick_list, manifest, scan_form, close_report, cod_report, dangerous_goods_report, order_summary, pickup_confirmation, dangerous_goods_declaration, consolidation_label, bill_of_lading, cmr, air_waybill, delivery_receipt, insurance_certificate, invoice, custom, other. Documents rendered from a template also carry template and template_version (chapter 25).
  • format: pdf, png, zpl, epl, gif, jpg. ZPL is returned as text at 203 dpi unless label.dpi is 300.
  • size: 4x6, 4x8, a4, a5, a6, letter, or WxH in inches.
  • url is a signed, short-lived URL. GET /v1/shipments/{id} re-signs it. Pass label.inline: true at purchase to get base64 in data instead.

11. References

reference is a string up to 35 characters that the carrier prints on the label. Carriers that support several get the references standardized feature of carrier_configs (§6.1, §6). Implementers truncate per carrier and report it in messages[].

12. Messages

Successful responses MAY carry non-fatal messages[]:
Levels: info, warning. Errors are never messages.

13. Recurrence

Defined in Platform Constructs → Shared objects. It is used here by pickup schedules, routes, driver shifts and end-of-day close.

14. Area

Defined in Platform Constructs → Shared objects. It is used here by zones.