Erin Claim System
Erin (erin.yodel.co.uk) is a unified REST API portal used by both Yodel and InPost for automated claim submission and monitoring. A single Temporal workflow — ErinClaimWorkflow — handles both couriers, routing each claim through the correct path based on claim type and client configuration.
| Couriers | Yodel, InPost |
| Portal | erin.yodel.co.uk (REST API) |
| Workflow | ErinClaimWorkflow (Temporal) |
| Clients | Any client with Yodel or InPost credentials |
| Trigger | Manual CLI backfill — no automatic schedule today |
| Supported claim reasons | Lost, Delivery Dispute, Damaged, Late |
Claim Types
The workflow handles two claim types, determined from ClaimReason.Name:
Same endpoint and payload shape for both types:
POST /api/gateway/claims/parcels/{altPclNo}/{instanceId}/claims
{ "type": "LOSS" | "DAMAGE", "costPrice": "28.00", "retailPrice": "35.00",
"itemDescription": "...", "description": "Parcel reported as lost/damaged.", "evidence": ["uuid1"] }Settlement amounts: Erin returns a
claimsLevelfield alongsidesettlementAmount. Settlement ismin(retailPrice, claimsLevel)— i.e. the claim is paid in full up to a per-category cap set in the portal contract. For example, M&S hamper claims have a £25 cap regardless of retail price (a £50 hamper settles at £25; a £20 hamper settles at £20). The cap is a Yodel contract configuration, not something we control.
How We Decide: Query First vs Direct Claim
This routing applies to LOSS claims only. DAMAGE claims always go direct.
We know this from evidence, not assumption. M&S is the strictest client we have on Erin — they have allowClaimsWithoutQueries=false, meaning every LOSS claim requires a query to be raised first before a formal claim can be submitted. Despite this, when we queried the full M&S DAMAGE claim history on Erin, we found 148 DAMAGE claims submitted between 2020 and 2026, none of which had a prior query. Every one went straight to claim submission and was accepted. If the query requirement applied to DAMAGE claims, at least some of those would have been rejected — they weren't.
This tells us the allowClaimsWithoutQueries flag is only evaluated by Erin for LOSS claims. DAMAGE claim submissions hit the same endpoint but Erin does not apply the query gate to them.
Before submitting any LOSS claim, the workflow calls the Erin validate endpoint:
GET /api/gateway/claims/{altPclNo}/{instanceId}/validateResponse fields used:
claimAllowed(bool) — whether Erin will accept a LOSS claim right nowclaimWindowPassed(bool) — whether the claim submission window has expiredmessageList(string array) — reason when claim is not allowed
Outcome routing table:
| claimAllowed | messageList contains | Meaning | Action |
|---|---|---|---|
true |
— | Client may submit directly | → Submit LOSS claim (Path A) |
false |
"A query must be raised on this parcel" | Client's contract requires query-first | → Raise query (Path B) |
false |
"A query was not raised within 14 days" | Claim window has passed | → Reject — Outside Window (status 9, reason 19) |
false |
Other | Already submitted or not eligible | → Log and end workflow |
Example — Path A client (e.g. Frasers Group / InPost):
Clients configured with allowClaimsWithoutQueries: true in the Erin portal get claimAllowed: true from the validate endpoint — Erin accepts the LOSS claim immediately with no query step required. Frasers Group is one such client (6 InPost contracts, all inpostClient: true).
Example — Path B client (e.g. M&S / Yodel):
Clients with allowClaimsWithoutQueries: false get claimAllowed: false with the message "A query must be raised on this parcel". Our code detects this specific message string and routes to Path B automatically. M&S is one such client. Any future Yodel or InPost client with the same contract configuration will follow the same path.
Important distinction — validate vs query eligibility:
The validate endpoint checks claim submission eligibility only. There is no time window restriction on raising queries. The "14 days" message only blocks formal LOSS claim submission — queries can still be raised at any time after that. Raising a query does not reopen the claim submission window.
How Claims Enter the System
Claims are created in Claimit when a client uploads them via the portal. They start in status New. A CLI command is run manually to find New claims and start Temporal workflows for each one.
backfill yodel ← picks up New + Investigating Yodel claims
backfill inpost ← picks up New + Investigating InPost claimsKnown gap: There is no automated schedule. New claims sit as
Newuntil an operator runs the backfill command manually.
What the backfill does per claim:
| Condition | Behaviour |
|---|---|
| Claim status = New | Sets IsBackfill=false → submission runs |
| Claim status = Investigating | Sets IsBackfill=true → skips submission, enters query polling only |
| DryRun config | Yodel: { DryRun: false }, InPost: { DryRun: false } |
Three Paths: Direct, Query-First, and Damage
Phase 1: Evidence Generation (All Paths)
Before any submission to Erin, two PDF documents must exist for the claim. These activities run in parallel and are fully idempotent — they skip silently if a matching file already exists.
Step 1a — Proof of Purchase (POP)
Activity: ErinGenerateProofOfPurchaseActivity
- Fetches customer name, delivery address, email, and phone from Erin API (
GetParcelDetailsAsync) - Renders a PDF from an HTML template via Playwright
- Uploads to Azure Blob Storage (
productioncontainer)
Guards:
- If the claim has no
ValueRetailand no POP file already exists → throws non-retryable failure (workflow cannot proceed) - If a
ProofOfPurchasefile already exists → skips (client may have uploaded their own)
Retry policy: 3 attempts · 30s → 60s → 120s backoff · 10 min timeout
Step 1b — Proof of Cost (POC)
Activity: ErinGenerateProofOfCostActivity
- Net cost =
ValueWholesaleCost; VAT derived at standard UK 20% rate; total shown on document - Renders PDF via Playwright, uploads to Azure Blob Storage
Guards:
- If the claim has no
ValueWholesaleCostand no POC file already exists → throws non-retryable failure - If a
ProofOfCostfile already exists → skips
Retry policy: 3 attempts · 30s → 60s → 120s backoff · 10 min timeout
Phase 2: Submit to Erin
Activity: SubmitClaimToErinActivity
This activity performs a five-step authenticated API sequence:
Retry policy: 3 attempts · 30s → 60s → 120s backoff · 5 min timeout
Submission Outcomes
| Outcome | What it means | What happens next |
|---|---|---|
Submitted |
Erin accepted the claim | Erin claimId stored as CourierClaimReference → enter Poll Claim Status |
Pending |
Parcel not found in Erin portal (e.g. not yet scanned at locker) | Workflow ends; claim stays as-is; retry by re-running backfill later |
Rejected |
Claim refused (e.g. return parcel / LRT service → HTTP 403) | Workflow ends; claim stays failed |
QueryRequired |
LOSS claim: client has allowClaimsWithoutQueries=false |
Route to Query Path (M&S) |
DAMAGE claims never produce a
QueryRequiredoutcome — they are accepted directly by Erin regardless of the client'sallowClaimsWithoutQueriessetting.
Path A: Direct Claim (LOSS and DAMAGE)
After a Submitted outcome, the workflow enters continuous polling to watch for Erin's decision.
Path B: Query Path (LOSS claims, M&S-style clients only)
When the submission outcome is QueryRequired, the workflow pivots to raising a formal query with Yodel before a claim can be submitted.
Step B1 — Raise Query
Activity: RaiseErinQueryActivity
- API call:
POST /api/gateway/parcels/{altPclNo}/{instanceId}/queries - Query type:
PARCEL_NOT_YET_DELIVERED- Falls back to
OTHERfor delivery disputes (Erin rejectsPARCEL_NOT_YET_DELIVEREDin that case)
- Falls back to
- Stores Erin
queryIdasCourierClaimReferencein the Claimit database - Claim transitions: New → Investigating
- Supported claim reasons: Lost, Delivery Dispute, Late
- DAMAGE claims never reach this path — they go direct regardless of client config
- Late claims always use
queryType=OTHER—PARCEL_NOT_YET_DELIVEREDis never attempted because the parcel was delivered
- Query wording by reason:
- Lost:
"Parcel {tracking} reported as not delivered. {SupportingCopy}" - Delivery Dispute:
"Our customer has not received their items and is therefore disputing delivery of parcel {tracking}. {SupportingCopy}" - Late:
"Parcel {tracking} was not delivered within the expected timeframe. {SupportingCopy}" SupportingCopyis appended when present (e.g. "NDD early/late - outside delivery window" from M&S goodwill spreadsheet import)
- Lost:
- Evidence upload for queries uses a different endpoint than claims:
- Claims evidence:
POST /api/gateway/claims/evidence→ response:{ status, data: { id, filename } } - Query attachments:
POST /api/gateway/queries/attachment→ response:{ id, filename }(flat — nodatawrapper)
- Claims evidence:
- Attachments must be posted as a response to be visible in the portal. The
attachmentsfield on the query creation body is accepted by the API without error but never displayed in the Erin portal UI. After raising the query, the workflow posts a follow-up response viaPOST /api/gateway/queries/{queryId}/responseswith{ content: "...", attachments: ["uuid1", ...] }— this is what makes the files visible to the Yodel team.- If existing queries are missing attachments, use CLI:
erin-attach-query-evidence --db-prod --claim <guid>
- If existing queries are missing attachments, use CLI:
Retry policy: 3 attempts · 30s → 60s → 120s backoff · 5 min timeout
Step B2 — Poll Query Responses
Activity: PollErinQueryResponsesActivity
Poll interval: every 12 hours · Max duration: 30 days
Each poll makes two API calls:
GET /api/gateway/queries/{queryId}/responses— fetch all responses from YodelGET /api/parcel-search/parcel-search+GET /api/gateway/parcels/{altPclNo}/{instanceId}/queries— check if the query has been closed
Retry policy: 3 attempts · 30s → 60s → 120s backoff · 5 min timeout
Decision Tree (evaluated in strict order)
AI Agent (Query Analysis)
Activity: RunErinQueryAgentActivity
Triggered when the "raise a claim" phrase is found. The agent analyses all Yodel responses to determine the appropriate claim outcome.
- Can propose
UpdateClaimStatuswith atarget_status_idand optionalrejection_reason_id - Failure is non-blocking — logs a warning, polling continues
- Retry policy: 2 attempts · 10s → 20s backoff · 10 min timeout · 30s heartbeat
Complete End-to-End Flow
Claim Status Transitions
| Trigger | From Status | To Status |
|---|---|---|
| Backfill starts workflow | New | (unchanged — workflow tracks internally) |
| LOSS or DAMAGE claim submitted to Erin | New | Submitted (3) |
| Query raised (M&S LOSS path) | New | Investigating (21) |
| Formal LOSS claim submitted after query trigger | Investigating | Submitted (3) |
Erin status: ACCEPTED_PENDING_PAYMENT |
Submitted | Accepted (5) — settlementAmount stored as credit amount |
Erin status: ACCEPTED_PAID |
Submitted or Accepted | Credited (6) — settlementAmount + creditNoteRef logged |
| Erin status: RESPONSE_REQUIRED | Submitted | Manual review (stays Submitted) |
| Erin status: REOPEN_BY_CLIENT | Submitted | Manual review (stays Submitted) |
| Query CLOSED + Yodel says "delivered" | Investigating | Found (12) |
| Validate: out-of-time-window | Investigating | Rejected (9) — reason: Outside Window |
| Parcel not at locker yet | New | (unchanged — workflow ends, retry later) |
| 90-day poll timeout | Submitted | Manual review (stays Submitted) |
| 30-day query timeout | Investigating | Manual review (stays Investigating) |
Activity Retry Policies
| Activity | Attempts | Backoff | Timeout | Heartbeat |
|---|---|---|---|---|
| Generate POP | 3 | 30s → 60s → 120s | 10 min | — |
| Generate POC | 3 | 30s → 60s → 120s | 10 min | — |
| Submit claim | 3 | 30s → 60s → 120s | 5 min | — |
| Raise query | 3 | 30s → 60s → 120s | 5 min | — |
| Poll query responses | 3 | 30s → 60s → 120s | 5 min | — |
| Check claim status | 3 | 30s → 60s → 120s | 5 min | — |
| AI agent | 2 | 10s → 20s | 10 min | 30s |
Error Scenarios
| Error | Behaviour |
|---|---|
No ValueRetail and no POP file |
Non-retryable failure. Workflow cannot proceed. |
No ValueWholesaleCost and no POC file |
Non-retryable failure. Workflow cannot proceed. |
| Parcel not found in Erin (Pending) | Workflow ends cleanly. Retry by re-running backfill. |
| Return parcel / LRT service (403) | Claim rejected. Workflow ends. |
| Validate: query not raised within 14 days | Transition to Rejected (Outside Window). Workflow ends. |
| REOPEN_BY_CLIENT status | Manual review required. Workflow ends. |
| Query CLOSED, no "delivered", no trigger | Manual review required. Workflow ends safely without guessing outcome. |
| AI agent failure | Non-blocking warning logged. Polling continues. |
| 90-day claim poll timeout | Manual review required. |
| 30-day query poll timeout | Manual review required. |
Technology
This system runs on the Temporal workflow engine, which provides durable execution, automatic retries, and long-running timer support without infrastructure risk. If the workflow process crashes, Temporal replays history and resumes exactly where it left off — including mid-poll waits of days or weeks.
Evidence PDFs are rendered server-side using Playwright (headless browser) and stored in Azure Blob Storage (production container). All Erin API access is authenticated per-client using credentials stored in ClientCourierLogins.