# Relay

How a parcel gets from a merchant's shelf to a doorstep, and what holds that together

> Generated by vibex 0.4.0 on 2026-09-26T07:20:54.460Z. Checked against `8bf79e9`.
>
> **153 claims** — 150 verified, 3 stated.
>
> Every statement carries its source. `✓` verified against the code · `·` stated by a person · `!` needs re-reading or out of date · `✗` unverifiable. Do not read an absence as a "no" — check what this document says it does not cover.

## The shape of it

*What runs, and why there are three front doors instead of one.*

Relay has three kinds of user and almost nothing in common between them. A merchant integrates once and calls an API for years. A recipient arrives from an SMS, has no account, and will never come back. A courier is offline half the day and uploads in bursts.

That is why there are three front doors rather than one, and why each authenticates differently: an API key for merchants ·, a one-time code for recipients ·, a client certificate for handhelds ·. Trying to serve all three from one authenticated API is the mistake this architecture exists to avoid.

Behind them, one service owns parcel state ✓ and one service owns the write path for movement ✓. Everything else reads.

### Evidence

- ✓ Merchant is a person. Books shipments and pays for them.  
  *verified against the code — computed from `relay-context.c4` by `c4.elements`.*
- ✓ Recipient is a person. Waits in, or does not.  
  *verified against the code — computed from `relay-context.c4` by `c4.elements`.*
- ✓ Courier is a person. Collects, sorts and delivers.  
  *verified against the code — computed from `relay-context.c4` by `c4.elements`.*
- ✓ Relay is a system. Moves parcels from a merchant's shelf to a doorstep, and bills for it.  
  *verified against the code — computed from `relay-context.c4` by `c4.elements`.*
- ✓ Partner carriers is a system, owned outside this system. Final mile in postcodes Relay does not serve.  
  *verified against the code — computed from `relay-context.c4` by `c4.elements`.*
- ✓ Loqate is a system, owned outside this system. Address validation and geocoding.  
  *verified against the code — computed from `relay-context.c4` by `c4.elements`.*
- ✓ Twilio is a system, owned outside this system. The SMS a recipient actually reads.  
  *verified against the code — computed from `relay-context.c4` by `c4.elements`.*
- ✓ Banking partner is a system, owned outside this system. Collects payment against an invoice.  
  *verified against the code — computed from `relay-context.c4` by `c4.elements`.*
- ✓ Merchant is a person. Books shipments and watches them until they land.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Recipient is a person. Tracks a parcel and reschedules when they will not be in.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Courier is a person. Scans parcels at every handover and records delivery attempts.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Merchant portal is a container, built with Next.js. Booking, labels, invoices and shipment history.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Tracking page is a container, built with React. The public page a recipient opens from an SMS. No login.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Courier handheld is a container, built with Kotlin / Android. Scans barcodes offline and syncs when it finds signal.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Shipment API is a container, built with Go. Booking, cancellation and the merchant-facing read model.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Scan ingest is a container, built with Go. Accepts batched, out-of-order scans from handhelds and makes them idempotent.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Dispatch is a container, built with Java / Spring. Owns parcel state and decides which route a parcel joins.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Routing engine is a container, built with Python. Sequences stops for a van under time windows and vehicle capacity.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Notifier is a container, built with Node.js. Sends the SMS and email a recipient actually reads.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Relay DB is a database, built with PostgreSQL 15. Shipments, parcels, routes and every scan ever taken.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Parcel events is a queue, built with Kafka. parcel.scanned, parcel.delivered, parcel.attempt-failed.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Loqate is a system, owned outside this system. Address validation and geocoding.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Twilio is a system, owned outside this system. SMS delivery to recipients.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Partner carriers is a system, owned outside this system. Final mile in postcodes Relay does not serve itself.  
  *verified against the code — computed from `relay.c4` by `c4.elements`.*
- ✓ Scan consumer is a component, built with Kafka listener. Reads parcel.scanned and orders events by when they happened, not when they arrived.  
  *verified against the code — computed from `dispatch.c4` by `c4.elements`.*
- ✓ State machine is a component, built with Java. The only thing that changes a parcel's status, and refuses any move the table does not permit.  
  *verified against the code — computed from `dispatch.c4` by `c4.elements`.*
- ✓ Attempt policy is a component, built with Java. Decides whether a failed delivery is retried tomorrow or parked for the recipient.  
  *verified against the code — computed from `dispatch.c4` by `c4.elements`.*
- ✓ Route planner is a component, built with Java. Places a parcel on a route and asks the routing engine to sequence it.  
  *verified against the code — computed from `dispatch.c4` by `c4.elements`.*
- ✓ Partner hand-off is a component, built with Java. Sends out-of-area parcels to a partner carrier and stops tracking them.  
  *verified against the code — computed from `dispatch.c4` by `c4.elements`.*
- ✓ Event publisher is a component, built with Kafka producer. Emits parcel.delivered, parcel.attempt-failed and parcel.exception after the state change commits.  
  *verified against the code — computed from `dispatch.c4` by `c4.elements`.*
- ✓ Parcel repository is a component, built with JDBC. The only component here that touches the database.  
  *verified against the code — computed from `dispatch.c4` by `c4.elements`.*
- ✓ Parcel events is a queue, built with Kafka, owned outside this system. Where scans arrive from, and where state changes go.  
  *verified against the code — computed from `dispatch.c4` by `c4.elements`.*
- ✓ Relay DB is a database, built with PostgreSQL 15, owned outside this system. The parcel, route and scan tables.  
  *verified against the code — computed from `dispatch.c4` by `c4.elements`.*
- ✓ Routing engine is a container, built with Python, owned outside this system. Sequences a van’s stops under time windows.  
  *verified against the code — computed from `dispatch.c4` by `c4.elements`.*
- ✓ The Relay system boundary contains Merchant portal, Tracking page, Courier handheld and Core services.  
  *verified against the code — computed from `relay.c4` by `c4.boundaries`.*
- ✓ The Core services deployment boundary contains Shipment API, Scan ingest, Dispatch, Routing engine, Notifier, Relay DB and Parcel events.  
  *verified against the code — computed from `relay.c4` by `c4.boundaries`.*
- ✓ The Dispatch container boundary contains Scan consumer, State machine, Attempt policy, Route planner, Partner hand-off, Event publisher and Parcel repository.  
  *verified against the code — computed from `dispatch.c4` by `c4.boundaries`.*
- · Merchants authenticate with a long-lived API key scoped to one merchant.  
  *stated by a person — stated by ana@example.com on 2026-09-19. stated by ana@example.com, confirmed 7 days ago.*
- · Recipients authenticate with a one-time code sent to the phone number on the shipment.  
  *stated by a person — stated by ana@example.com on 2026-09-19. stated by ana@example.com, confirmed 7 days ago.*
- · Handhelds authenticate with a per-device client certificate.  
  *stated by a person — stated by ana@example.com on 2026-09-19. stated by ana@example.com, confirmed 7 days ago.*
- ✓ Dispatch is the only service that changes a parcel's status.  
  *verified against the code — [`dispatch/src/main/java/relay/ParcelStateMachine.java › ParcelStateMachine`](https://github.com/example/relay/blob/8bf79e90d3a38da172837d18ef66c46b82e7f2e1/dispatch/src/main/java/relay/ParcelStateMachine.java).*
- ✓ Scan ingest is the only write path for parcel movement.  
  *verified against the code — [`src/scans/ingest.go › Ingest`](https://github.com/example/relay/blob/8bf79e90d3a38da172837d18ef66c46b82e7f2e1/src/scans/ingest.go).*

## What actually moves a parcel

*The one rule that makes the status field mean something.*

**Only a scan moves a parcel forward.** There is no manual status edit anywhere in Relay ✓, and the transition table is enforced in code rather than by convention ✓.

This is not fussiness. A status that a human could set by hand would not be evidence that a parcel was physically anywhere, and every downstream promise — the delivery window, the merchant's invoice, the insurance claim — is built on it being exactly that.

The cost is that a parcel with a broken barcode is genuinely stuck until someone scans it, and support cannot type their way out. That trade is deliberate.

> Two attempts, then the parcel stops moving until a person chooses ✓. Van time is the scarce resource here, not shelf space at a depot, so a third speculative attempt costs more than waiting.

### Evidence

- ✓ Created is the state a new Parcel starts in. 2 transitions lead out of it. Booked and labelled, still on the merchant's shelf. Acts next: merchant.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ Collected is a state. 1 transition leads out of it. In Relay's hands, not yet sorted. Acts next: courier.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ At depot is a state. 3 transitions lead out of it. Sorted and waiting for the next leg. Acts next: system.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ In transit is a state. 2 transitions lead out of it. On a trunk vehicle between two depots. Acts next: system.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ Out for delivery is a state. 2 transitions lead out of it. On a van, with a sequenced stop and a promised window. Acts next: courier.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ Attempt failed is a failure state. 2 transitions lead out of it. Nobody in, or no safe place. Back to the depot tonight. Acts next: system.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ Awaiting instruction is a state that waits on something outside the system. 2 transitions lead out of it. Parked until the recipient chooses what happens next. Acts next: recipient.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ Returning is a state. 1 transition leads out of it. Heading back to the merchant. Acts next: system.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ Delivered is a final state. Nothing leaves it. Handed over, or left in the agreed safe place with a photo.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ Returned is a final state. Nothing leaves it. Back with the merchant. Charged as a failed delivery.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ Cancelled is a final state. Nothing leaves it. Withdrawn before Relay ever took it.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ Lost is a failure state. 1 transition leads out of it. No scan for ten days. A claim is opened automatically.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.states`.*
- ✓ Draft is the state a new Invoice starts in. 1 transition leads out of it. Accruing lines as parcels are delivered. Acts next: system.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.states`.*
- ✓ Issued is a state that waits on something outside the system. 3 transitions lead out of it. Sent, and waiting on the merchant's terms. Acts next: merchant.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.states`.*
- ✓ Part paid is a state. 2 transitions lead out of it. Some money arrived, a balance remains. Acts next: merchant.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.states`.*
- ✓ Overdue is a failure state. 2 transitions lead out of it. Past its due date with a balance outstanding. Acts next: credit control.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.states`.*
- ✓ Paid is a final state. Nothing leaves it. Settled in full.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.states`.*
- ✓ Written off is a final state. Nothing leaves it. Given up on, and the account put on hold.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.states`.*
- ✓ Created moves to Collected on collection scan. Triggered by: courier. Side effect: start the clock on the service promise.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ Created moves to Cancelled on cancel. Triggered by: merchant. Only when: no scan has been taken. Side effect: void the label.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ Collected moves to At depot on depot_inbound scan. Triggered by: courier.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ At depot moves to In transit on depot_outbound scan. Triggered by: system. Only when: the destination depot is not this one.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ In transit moves to At depot on depot_inbound scan. Triggered by: courier.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ At depot moves to Out for delivery on vehicle_load scan. Triggered by: courier. Only when: a stop exists on today's route. Side effect: text the recipient their two-hour window.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ Out for delivery moves to Delivered on delivery scan. Triggered by: courier. Side effect: publish parcel.delivered and notify the merchant.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ Out for delivery moves to Attempt failed on attempt_failed scan. Triggered by: courier. Side effect: record the reason and a doorstep photo.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ Attempt failed moves to Out for delivery on next working day. Triggered by: system. Only when: fewer than two attempts used. Side effect: add a stop to tomorrow's route.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ Attempt failed moves to Awaiting instruction on two attempts used. Triggered by: system. Side effect: text the recipient their options.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ Awaiting instruction moves to Out for delivery on reschedule or redirect. Triggered by: recipient. Side effect: book the chosen day and confirm by SMS.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ Awaiting instruction moves to Returning on five working days elapsed. Triggered by: system. Side effect: notify the merchant that it is coming back.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ Returning moves to Returned on return scan at origin depot. Triggered by: courier. Side effect: raise the return charge.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ In transit moves to Lost on ten days without a scan. Triggered by: system.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ At depot moves to Lost on ten days without a scan. Triggered by: system. Side effect: open a claim and credit the merchant.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ Lost moves to At depot on any scan. Triggered by: courier. Side effect: close the claim as found.  
  *verified against the code — computed from `parcel.lifecycle` by `lifecycle.transitions`.*
- ✓ Draft moves to Issued on month closes. Triggered by: system. Only when: the invoice has at least one line. Side effect: email the merchant and start the terms clock.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.transitions`.*
- ✓ Issued moves to Paid on payment matches the total. Triggered by: banking partner.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.transitions`.*
- ✓ Issued moves to Part paid on payment is less than the total. Triggered by: banking partner. Side effect: email a statement of the balance.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.transitions`.*
- ✓ Part paid moves to Paid on balance reaches zero. Triggered by: banking partner.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.transitions`.*
- ✓ Issued moves to Overdue on due date passes. Triggered by: system. Side effect: notify credit control.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.transitions`.*
- ✓ Part paid moves to Overdue on due date passes. Triggered by: system.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.transitions`.*
- ✓ Overdue moves to Paid on balance recovered. Triggered by: credit control. Side effect: release the account hold.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.transitions`.*
- ✓ Overdue moves to Written off on 90 days overdue. Triggered by: credit control. Side effect: suspend the account.  
  *verified against the code — computed from `invoice.lifecycle` by `lifecycle.transitions`.*
- ✓ No endpoint or admin tool can set a parcel's status directly.  
  *verified against the code — [`dispatch/src/main/java/relay/ParcelStateMachine.java › ALLOWED`](https://github.com/example/relay/blob/8bf79e90d3a38da172837d18ef66c46b82e7f2e1/dispatch/src/main/java/relay/ParcelStateMachine.java).*
- ✓ A transition the table does not permit is refused rather than logged and applied.  
  *verified against the code — [`dispatch/src/main/java/relay/ParcelStateMachine.java › permits`](https://github.com/example/relay/blob/8bf79e90d3a38da172837d18ef66c46b82e7f2e1/dispatch/src/main/java/relay/ParcelStateMachine.java).*
- ✓ A van attempts a door twice before the parcel stops moving.  
  *verified against the code — [`dispatch/src/main/java/relay/AttemptPolicy.java › MAX_ATTEMPTS`](https://github.com/example/relay/blob/8bf79e90d3a38da172837d18ef66c46b82e7f2e1/dispatch/src/main/java/relay/AttemptPolicy.java).*

## Scans arrive late and out of order

*The assumption that shapes ingest, and the bug it prevents.*

A handheld in a depot basement holds its scans and uploads when it finds signal. So a batch can contain scans older than ones already recorded, and the same batch can arrive twice when an upload times out on a bad connection.

Both are handled at the ingest boundary. Every scan carries a key that is stable across retries, and a unique index is what enforces exactly-once — a duplicate loses the race and is discarded ✓. A scan describing a step the parcel has already passed is still recorded, because the history has to be complete, but it does not move the status backwards ✓.

The practical consequence for anyone reading this data: `scanned_at` is device time and can be wrong ✓, while `received_at` is set by the server and cannot be ✓. Order a tracking history by the first and reason about lateness with the second.

### Evidence

- ✓ The merchants table has 4 columns and is keyed on id. account_ref is unique.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The shipments table has 7 columns and is keyed on id. reference may be null.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The parcels table has 7 columns and is keyed on id. tracking_number is unique. current_depot_id may be null.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The recipients table has 4 columns and is keyed on id. phone and email may be null.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The addresses table has 9 columns and is keyed on id. line2, latitude, longitude and validated_at may be null.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The depots table has 4 columns and is keyed on id. code is unique.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The drivers table has 4 columns and is keyed on id. licence_ref is unique.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The routes table has 5 columns and is keyed on id. driver_id and sequenced_at may be null.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The stops table has 6 columns and is keyed on id.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The scans table has 9 columns and is keyed on id. idempotency_key is unique. depot_id and driver_id may be null.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The delivery_attempts table has 6 columns and is keyed on id. stop_id and photo_url may be null.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The parcel_status enum allows 12 values: created, collected, at_depot, in_transit, out_for_delivery, delivered, attempt_failed, awaiting_instruction, returning, returned, lost and cancelled.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The scan_type enum allows 7 values: collection, depot_inbound, depot_outbound, vehicle_load, delivery, attempt_failed and return.  
  *verified against the code — computed from `relay.erd` by `erd.entities`.*
- ✓ The accounts table has 6 columns and is keyed on id. merchantId is unique.  
  *verified against the code — computed from `billing.erd` by `erd.entities`.*
- ✓ The invoices table has 7 columns and is keyed on id. number is unique. issuedAt and dueAt may be null.  
  *verified against the code — computed from `billing.erd` by `erd.entities`.*
- ✓ The invoice_lines table has 6 columns and is keyed on id.  
  *verified against the code — computed from `billing.erd` by `erd.entities`.*
- ✓ The payments table has 5 columns and is keyed on id. reference is unique.  
  *verified against the code — computed from `billing.erd` by `erd.entities`.*
- ✓ The rate_cards table has 6 columns and is keyed on id. validTo may be null.  
  *verified against the code — computed from `billing.erd` by `erd.entities`.*
- ✓ The InvoiceStatus enum allows 6 values: draft, issued, part_paid, paid, overdue and written_off.  
  *verified against the code — computed from `billing.erd` by `erd.entities`.*
- ✓ Each shipments row relates to exactly one merchants row. Each merchants row relates to any number of shipments rows. The link is shipments.merchant_id → merchants.id. A merchants row cannot be deleted while shipments rows reference it. The relationship is labelled "booked by".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each shipments row relates to exactly one recipients row. Each recipients row relates to any number of shipments rows. The link is shipments.recipient_id → recipients.id. The relationship is labelled "addressed to".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each shipments row relates to exactly one addresses row. Each addresses row relates to any number of shipments rows. The link is shipments.destination_id → addresses.id. The relationship is labelled "delivers to".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each parcels row relates to exactly one shipments row. Each shipments row relates to any number of parcels rows. The link is parcels.shipment_id → shipments.id. Deleting a shipments row deletes the parcels rows that reference it. The relationship is labelled "part of".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each parcels row relates to at most one depots row. Each depots row relates to any number of parcels rows. The link is parcels.current_depot_id → depots.id. The relationship is labelled "currently at".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each depots row relates to exactly one addresses row. Each addresses row relates to any number of depots rows. The link is depots.address_id → addresses.id. The relationship is labelled "sited at".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each drivers row relates to exactly one depots row. Each depots row relates to any number of drivers rows. The link is drivers.depot_id → depots.id. The relationship is labelled "based at".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each routes row relates to exactly one depots row. Each depots row relates to any number of routes rows. The link is routes.depot_id → depots.id. The relationship is labelled "leaves from".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each routes row relates to at most one drivers row. Each drivers row relates to any number of routes rows. The link is routes.driver_id → drivers.id. The relationship is labelled "driven by".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each stops row relates to exactly one routes row. Each routes row relates to any number of stops rows. The link is stops.route_id → routes.id. Deleting a routes row deletes the stops rows that reference it. The relationship is labelled "on".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each stops row relates to exactly one parcels row. Each parcels row relates to any number of stops rows. The link is stops.parcel_id → parcels.id. The relationship is labelled "delivers".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each scans row relates to exactly one parcels row. Each parcels row relates to any number of scans rows. The link is scans.parcel_id → parcels.id. A parcels row cannot be deleted while scans rows reference it. The relationship is labelled "records".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each delivery_attempts row relates to exactly one parcels row. Each parcels row relates to any number of delivery_attempts rows. The link is delivery_attempts.parcel_id → parcels.id. The relationship is labelled "against".  
  *verified against the code — computed from `relay.erd` by `erd.relationships`.*
- ✓ Each invoices row relates to exactly one accounts row. Each accounts row relates to any number of invoices rows. The link is invoices.accountId → accounts.id. The relationship is labelled "account".  
  *verified against the code — computed from `billing.erd` by `erd.relationships`.*
- ✓ Each invoice_lines row relates to exactly one invoices row. Each invoices row relates to any number of invoice_lines rows. The link is invoice_lines.invoiceId → invoices.id. Deleting a invoices row deletes the invoice_lines rows that reference it. The relationship is labelled "invoice".  
  *verified against the code — computed from `billing.erd` by `erd.relationships`.*
- ✓ Each payments row relates to exactly one invoices row. Each invoices row relates to any number of payments rows. The link is payments.invoiceId → invoices.id. The relationship is labelled "invoice".  
  *verified against the code — computed from `billing.erd` by `erd.relationships`.*
- ✓ Each rate_cards row relates to exactly one accounts row. Each accounts row relates to any number of rate_cards rows. The link is rate_cards.accountId → accounts.id. The relationship is labelled "account".  
  *verified against the code — computed from `billing.erd` by `erd.relationships`.*
- ✓ A scan batch can be uploaded any number of times and is recorded exactly once.  
  *verified against the code — [`src/scans/ingest.go › IdempotencyKey`](https://github.com/example/relay/blob/8bf79e90d3a38da172837d18ef66c46b82e7f2e1/src/scans/ingest.go).*
- ✓ A scan describing a step the parcel has already passed is recorded but does not move the status backwards.  
  *verified against the code — [`dispatch/src/main/java/relay/ParcelStateMachine.java › apply`](https://github.com/example/relay/blob/8bf79e90d3a38da172837d18ef66c46b82e7f2e1/dispatch/src/main/java/relay/ParcelStateMachine.java).*
- ✓ scanned_at comes from the handheld's own clock and can be wrong.  
  *verified against the code — [`src/scans/ingest.go › ScannedAt`](https://github.com/example/relay/blob/8bf79e90d3a38da172837d18ef66c46b82e7f2e1/src/scans/ingest.go).*
- ✓ received_at is set by the server when a batch arrives and cannot be influenced by a device.  
  *verified against the code — [`src/scans/ingest.go › Ingest`](https://github.com/example/relay/blob/8bf79e90d3a38da172837d18ef66c46b82e7f2e1/src/scans/ingest.go).*

## The API surface

*Every operation, grouped by who is allowed to call it.*

Cancellation is the one merchant operation with a hard edge: it is only honoured before the first scan ✓. After that the parcel exists in the network and has to come back through it, which is a return and is charged as one.

### Evidence

- ✓ POST /v1/shipments — Book a shipment and mint a tracking number per parcel. It accepts CreateShipmentInput. It returns Shipment.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ GET /v1/shipments — List the calling merchant's shipments, newest first. It returns Shipment[].  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ GET /v1/shipments/{id} — One shipment with all of its parcels. Required parameter: id (path). It returns Shipment.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ POST /v1/shipments/{id}/cancel — Cancel before collection; fails once any parcel has been scanned. Required parameter: id (path). It returns Shipment.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ GET /v1/parcels/{id}/label — The printable label as a PDF. Required parameter: id (path). It returns application/pdf.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ GET /v1/track/{tracking_number} — Public tracking history for one parcel. Required parameter: tracking_number (path). It is callable without authentication. It returns TrackingEvent[].  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ POST /v1/track/{tracking_number}/reschedule — Pick another delivery day. Required parameter: tracking_number (path). It accepts RescheduleInput. It returns TrackingEvent.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ POST /v1/track/{tracking_number}/redirect — Send it to a neighbour or a safe place instead. Required parameter: tracking_number (path). It accepts RedirectInput. It returns TrackingEvent.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ POST /v1/scans — Upload a batch of scans taken while offline. Required parameter: Idempotency-Key (header). It accepts ScanBatch. It returns ScanBatchResult.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ POST /v1/attempts — Record a failed delivery attempt with a reason and a photo. It accepts AttemptInput. It returns TrackingEvent.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ GET /v1/routes/{id}/manifest — Today's sequenced stops for a driver, cacheable offline. Required parameter: id (path). It returns Stop[].  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ GET /v1/depots — Every depot and the postcodes it serves. It returns Depot[].  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ POST /v1/routes/{id}/sequence — Ask the routing engine to order the stops. Required parameter: id (path). It returns Route.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ GET /track/{tracking_number} — Unversioned tracking; use /v1/track. Required parameter: tracking_number (path). It is callable without authentication. It returns TrackingEvent[]. It is deprecated.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ EVENT parcel.delivered — Published when a delivery scan lands. It returns ParcelDeliveredEvent.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ EVENT parcel.attempt-failed — Published when a courier records a failed attempt. It returns ParcelAttemptEvent.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ EVENT parcel.exception — Published when a parcel is marked lost or begins returning. It returns ParcelExceptionEvent.  
  *verified against the code — computed from `relay.endpoints` by `endpoints.operations`.*
- ✓ QUERY shipment(id: ID!) — One shipment with its parcels and their current state. Required parameter: id (arg). It returns Shipment.  
  *verified against the code — computed from `portal.endpoints` by `endpoints.operations`.*
- ✓ QUERY shipments(status: ParcelStatus, bookedSince: DateTime, first: Int, after: String) — The calling merchant's shipments, newest first. It returns ShipmentConnection!.  
  *verified against the code — computed from `portal.endpoints` by `endpoints.operations`.*
- ✓ QUERY parcel(trackingNumber: String!) — Everything known about one parcel, by its tracking number. Required parameter: trackingNumber (arg). It returns Parcel.  
  *verified against the code — computed from `portal.endpoints` by `endpoints.operations`.*
- ✓ QUERY depots — Depots and the postcodes they serve. It returns [Depot!]!.  
  *verified against the code — computed from `portal.endpoints` by `endpoints.operations`.*
- ✓ MUTATION bookShipment(input: BookShipmentInput!) — Book a shipment and mint a tracking number per parcel. Required parameter: input (arg). It returns BookShipmentPayload!.  
  *verified against the code — computed from `portal.endpoints` by `endpoints.operations`.*
- ✓ MUTATION cancelShipment(id: ID!) — Cancel before collection; fails once any parcel has been scanned. Required parameter: id (arg). It returns CancelShipmentPayload!.  
  *verified against the code — computed from `portal.endpoints` by `endpoints.operations`.*
- ✓ MUTATION sequenceRoute(routeId: ID!) — Ask the routing engine to sequence a route's stops. Required parameter: routeId (arg). It returns Route!.  
  *verified against the code — computed from `portal.endpoints` by `endpoints.operations`.*
- ✓ SUBSCRIPTION parcelScanned(shipmentId: ID) — Every scan on the calling merchant's parcels, as it is ingested. It returns Scan!.  
  *verified against the code — computed from `portal.endpoints` by `endpoints.operations`.*
- ✓ A shipment can be cancelled only until one of its parcels has been scanned.  
  *verified against the code — [`src/shipments/shipments.handler.go › AnyParcelScanned`](https://github.com/example/relay/blob/8bf79e90d3a38da172837d18ef66c46b82e7f2e1/src/shipments/shipments.handler.go).*

## What this document does not cover

Read this before trusting an absence. Something missing here is either out of scope below, listed as noticed-and-unaccounted-for, or nobody has looked.

### What was read

- **Relay in its world** — c4, 8 subjects, facts derived from it
- **Relay — parcel network** — c4, 18 subjects, facts derived from it
- **Inside Dispatch** — c4, 11 subjects, facts derived from it
- **Relay data model** — erd, 13 subjects, facts derived from it
- **Billing data model** — erd, 6 subjects, facts derived from it
- **Relay API** — endpoints, 25 subjects, facts derived from it
- **Merchant portal API** — endpoints, 27 subjects, facts derived from it
- **The journey of a parcel** — lifecycle, 12 subjects, facts derived from it
- **An invoice, month by month** — lifecycle, 6 subjects, facts derived from it

### Deliberately not covered

- **Pricing and invoicing** — How a shipment becomes a line on a merchant's bill is owned by the finance platform and documented there. Nothing here describes rate cards or surcharges.
- **Route optimisation** — The routing engine's algorithm is not documented here. This document says a route gets sequenced; it does not say how.
- **Partner carrier hand-off** — Not documented yet. Parcels for postcodes Relay does not serve leave the network and the tracking history goes quiet, and no claim here explains what happens to them.
- **Claims and insurance** — A lost parcel opens a claim. The claims process itself lives with the commercial team and is not modelled in this system.
- **How handhelds are provisioned** — Client certificates are issued and rotated by field systems. Nothing here covers enrolment, revocation, or what happens to a stolen device.
- **Billing internals** — The billing data model is drawn here, imported straight from its Prisma schema, but no claim explains how a rate card becomes an invoice line. That lives with the finance platform.

### Noticed and unaccounted for

- **relay.endpoints: 8 of 25 subjects have no claim** — nothing is documented about CreateShipmentInput, Shipment, Parcel, TrackingEvent, ScanBatch, …
- **portal.endpoints: 19 of 27 subjects have no claim** — nothing is documented about DateTime, Shipment, ShipmentConnection, Parcel, Scan, …

## Glossary

- **Scan** — A barcode read by a courier's handheld. The only evidence Relay accepts that a parcel was physically somewhere.
- **Depot** — A sorting site. Every parcel passes through at least one, usually two.
- **Trunk** — A vehicle moving parcels between depots rather than to doorsteps.
- **Stop** — One parcel's place in a driver's sequenced day, with the two-hour window promised to the recipient.
