# Mail stream

A postcard's journey after the send: USPS scans you record, and the cancellation window that closes the day after its send date.

The mail stream is what happens to a postcard after you send it: its USPS scans and the window in which you can still cancel it. Creating a postcard returns it with `status` `processed`, and it stays there. Record each scan with `POST /_tracking_event`, and cancel the postcard with `DELETE /postcards/{id}` while its window is open:

```bash
curl -s -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name":"design_studio"}' -o /dev/null
curl -s -u test_anything: "$RYSTIC_URL/addresses" -d name='Harry Zhang' -d address_line1='210 King St' -d address_city='San Francisco' -d address_state=CA -d address_zip=94107 | jq -c '{id}'
# -> {"id":"adr_0000000000000001"}
curl -s -u test_anything: "$RYSTIC_URL/postcards" -d to=adr_0000000000000001 -d from=adr_0000000000000001 -d front='<html>Hello</html>' -d back='<html>World</html>' -d use_type=operational | jq -c '{id, status, send_date, tracking_events}'
# -> {"id":"psc_0000000000000001","status":"processed","send_date":"2026-03-01T00:05:00.002Z","tracking_events":[]}
curl -s -X POST "$RYSTIC_URL/_tracking_event" -H 'Content-Type: application/json' -d '{"postcard":"psc_0000000000000001","name":"In Transit","location":"94107"}' | jq -c '{name, location, type}'
# -> {"name":"In Transit","location":"94107","type":"normal"}
curl -s -u test_anything: "$RYSTIC_URL/postcards/psc_0000000000000001" | jq -c '{status, scans: [.tracking_events[].name]}'
# -> {"status":"processed","scans":["In Transit"]}
curl -s -u test_anything: -X DELETE "$RYSTIC_URL/postcards/psc_0000000000000001" | jq -c .
# -> {"deleted":true,"id":"psc_0000000000000001"}
```

**Differs from Lob:** Lob leaves `tracking_events` empty on a test-mode postcard, and a live postcard gets its scans from USPS by itself. The simulator serves the scans a scenario or seed carries and the ones you post.

### Record a scan

`POST /_tracking_event` appends a `normal` tracking event to a postcard, so the postcard's next read serves it. Its body, form-encoded or JSON with `Content-Type: application/json`, takes these fields:

| Field | |
|---|---|
| `postcard` | required: the postcard's id, `psc_…` |
| `name` | required: one of the scans Lob documents, listed below |
| `location` | optional: a ZIP code. The event's `location` reads null without it. |
| `time` | optional: an RFC 3339 time. It defaults to the simulator's clock. |

The scans `name` takes:

- `Mailed`
- `In Transit`
- `In Local Area`
- `Processed for Delivery`
- `Delivered`
- `Re-Routed`
- `Returned to Sender`
- `International Exit`

A scan moves no other field and sends no webhook.

| The body | The route returns |
|---|---|
| has no `postcard`, a `name` not in the list or a `time` that is not RFC 3339 | 422 `invalid` |
| has a `postcard` the simulator does not hold | 404 `not_found` |
| has a `postcard` that failed or was canceled | 409 `invalid_state`, since a canceled piece is never mailed |

This is a simulator route, not a Lob one: it takes no Lob key, and under `RYSTIC_ADMIN_TOKEN` a request needs that value in `X-Rystic-Admin-Token`, as every control route does. A scenario timeline can call it as one of its steps.

### The cancellation window

A postcard stays cancelable through the end of its `send_date` day, in UTC:

| You cancel a postcard | `DELETE /postcards/{id}` returns |
|---|---|
| on or before its `send_date` day | `{"deleted": true, "id": …}` |
| from the next day on, or when it has no `send_date` | 422 `not_deletable`, `postcard can no longer be canceled` |

**Differs from Lob:** Lob refuses a cancel once `send_date` has passed, while the simulator checks only the calendar day.

A canceled postcard stays retrievable with `deleted` true and leaves the lists. Move the clock into the day after the `send_date` with `POST /_rystic/clock` to see the window close.

A postcard created with a future `send_date`, such as the `design_studio` card to Atlas Moving scheduled for `2026-03-09`, stays cancelable until the end of that day.

### Seeded outcomes

A seeded postcard can carry any stage you need, a captured `rendered` or `failed` outcome included, because `POST /_rystic/seed` writes the record as given. That replays an outcome deterministically; it is not a rendering clock, and the simulator never moves a postcard from `processed` to `rendered` on its own.

---

Source: https://www.rystic.ai/docs/simulators/lob/features/mail-stream · Markdown: https://www.rystic.ai/docs/simulators/lob/features/mail-stream.md
