# ADR 004: EasyPost for Shipping Labels

Date: 2026-04-16
Status: Accepted

## Context

Layer 5 introduces seller-driven shipping: a store owner needs to click one button, get the cheapest available label for a paid Order, and trigger the Stripe Transfer tied to that ship event. The chosen provider must also emit tracker webhooks so we can mark orders `delivered` without polling.

We evaluated EasyPost and Shippo. Both expose multi-carrier rates and tracker webhooks over HTTP. EasyPost's PHP SDK is officially maintained (`easypost/easypost-php`), signs webhooks with HMAC-SHA256 (matches the shape we already use for Stripe), and has lower friction for US-only Priority Mail — our launch target.

## Decision

- Use **EasyPost** as the only provider in production.
- Abstract behind a project-owned `LabelProvider` contract so the rest of the codebase depends on `App\Modules\Shipping\Contracts\LabelProvider`, not on EasyPost types.
- Ship a `FakeLabelProvider` that implements the same contract with deterministic fixtures. Bound in `local` and `testing` environments, or any time `EASYPOST_API_KEY` is unset.
- Webhook signature verification uses HMAC-SHA256 against `EASYPOST_WEBHOOK_SECRET`. Idempotency uses a Redis `SET NX EX 86400` keyed on `easypost:event:{event_id}`.

## Consequences

**Positive**

- One code path for tests and real shipping — no conditional "if easypost enabled" branches.
- Swapping to Shippo later means implementing the contract, not rewriting call sites.
- The Fake provider lets local dev and CI purchase labels without burning test-API credit.

**Negative**

- We lock into EasyPost's fee structure and carrier lineup for v1. International / freight expansion may require a second provider (and likely a non-trivial contract evolution).
- The contract is minimal (buy-cheapest, parse-event, verify-signature). Features like label voiding, rate shopping with shipping filters, or return labels will require extending both the contract and both adapters.

**Neutral**

- Webhook verification and idempotency match the Stripe webhook pattern already in the codebase — operations cost is familiar.
