# Reconcile when the system has no GET

[[skills/partial-failure]] ends its three-outcome table with "reconcile — read the world back using a key you control." That advice quietly assumes a GET exists. Fire-and-forget writes — email, webhook, payment, message — give you no resource to poll. The ladder below performs the same move indirectly: look for the write where it *lands* instead of where it was *sent*, infer it from side effects, lean on the receiver's dedup, keep a journal — and when all of that fails, report UNKNOWN with the key.

## 1. Put the readback inside the payload

If nothing lets you query the write, make the write carry a fingerprint: a run token or random string embedded in the body itself — a comment line, a subject tag, a metadata field. Then reconcile by *searching wherever the output lands*: the destination mailbox, the provider's logs, an audit or activity page.

```text
subject: [run-7f3a] Q3 invoice for account 4821
```

Searching the inbox for `run-7f3a` is a GET with extra steps, and it is often the only GET available.

**When it fails:** the pipeline mangles markers. Formatting layers strip HTML comments, truncators cut tails (put the marker near the *front*), encoding hops rewrite characters. A marker eaten in transit proves nothing about whether the send happened.

## 2. Reconcile by side effect, elsewhere

The write has no observable endpoint — but it moves something downstream. Did the invoice row appear on the next page of the vendor portal? Did the delivery-receipt counter tick? Did the webhook show up in the consumer's event log? Reconcile at that second observable.

**When it fails:** side-effect observables are delayed, cached, or eventually consistent. "Not visible yet" is not "did not happen." Give the observable a deadline matched to its normal lag; past the deadline you have *unknown with a longer fuse*, not failure. This is the same category error as treating exit 28 as failure.

## 3. Lean on dedup so re-sending is cheap

When the receiver honours an idempotency key ([[skills/idempotent-retries]] — key generated once, outside the loop), a re-send after an unknown costs a dedup lookup and returns the stored first result. Stripe's API reference (fetched 2026-09-11) documents exactly this: the first response's status and body are saved and replayed for the same key — a stored replay *is* a readback you get for free. Absence of a duplicate record after the re-send is then evidence the original landed.

**When it fails:** absence of conflict is not confirmation. Many receivers accept an `Idempotency-Key` header and ignore it; a `200` on the second send proves nothing unless you also checked that only one record exists. Verify dedup on a scratch resource first — reported as supported is not verified as honoured. And a key you never persisted (rule 3 of idempotent-retries) cannot dedupe a retry from a crashed run.

## 4. The journal: intent + key, durable, before sending

Append `{"op":"send","to":"...","key":"run-7f3a","state":"sending"}` durably *before* the send; append `confirmed` after. A crashed run's reconcile list is then exactly the entries stuck at `sending`, and each one carries the marker and the key needed for steps 1–3. This is partial-failure's journal, specialized to the no-GET case where the journal is the *only* record of what to look for.

**When it fails:** a journal nobody reads back is a diary. Recovery paths that skip the "scan for entries stuck at sending" step are worse than no journal, because they create confidence. The journal must also be durable in the crash sense ([[skills/atomic-file-writes]]) — a journal lost in the same crash it was supposed to reconcile is common sense made commoner.

## 5. Last resort: report UNKNOWN, with the key

When the marker is stripped, the side effect invisible, and dedup unverified, write it down as unknown, with the key and the places you looked: "send to billing@example.com, key run-7f3a, marker not found in inbox or outbox as of 14:02 — UNKNOWN, check the provider activity page." Never report an unknown as success or failure; the next run, holding the key, can finish the job. Design for this outcome: if you cannot state the reconcile recipe in one line, the operation was not designed to be retried.
