Zero-Day Retention
How zero-day-retention lines behave — what content is never persisted, where it's shown once, and what changes about polls, cards, and attachments.
What zero-day retention is
Section titled “What zero-day retention is”Zero-day retention means message content is retained for zero days — it is never persisted past the moment it’s needed, rather than being stored and later deleted or expired on a schedule.
On Linq, that means message and attachment content is never written to the database at all. It exists for exactly one moment on each side — synchronously, in the API response when you send it, or in a webhook when you receive it — and once that moment has passed, Linq no longer holds it anywhere. A later GET, list, or thread call for that same item returns the content fields empty, because there’s nothing behind them to return.
This page covers every content type this applies to today, what still persists (poll votes, reactions, and attachment metadata), and the two behavior changes it introduces for polls and iMessage app cards.
The rule, in one table
Section titled “The rule, in one table”| Outbound (you send it) | Inbound (you receive it) | |
|---|---|---|
| Content shown once, where | The synchronous response to your POST/PATCH call |
The first webhook about it (e.g. message.received) |
Later webhooks (.sent, .delivered, .read) |
Content empty | Content still real — see Inbound webhooks always carry the real content |
Later GET / list / thread |
Content empty | Content empty |
| Database | Never written | Never written |
Two different mechanisms produce the same guarantee, and it’s worth being precise about which applies where:
- Outbound: your own request already had the real content — the response just echoes it back to you once, in memory. Nothing about deletion is involved.
- Inbound: the first webhook for an item is built from the live event as it arrives, before anything is written to the database — so it still carries the real content. Every webhook after that first one, and every
GET/list/thread call at any point, reads from the database, which never had it.
How to tell a line is zero-day-retention
Section titled “How to tell a line is zero-day-retention”Every affected webhook event carries a zero_retention boolean. There is no separate endpoint to query a phone number’s retention setting directly — treat this field as the source of truth, per event, for whether that specific payload’s content was ever going to be stored.
{ "event_type": "message.sent", "data": { "id": "8f14e45f-ceea-467e-adc0-000000000001", "direction": "outbound", "parts": [], "zero_retention": true }}{ "event_type": "message.sent", "data": { "id": "8f14e45f-ceea-467e-adc0-000000000002", "direction": "outbound", "parts": [ { "type": "text", "value": "Running 10 minutes late!" } ], "zero_retention": false }}What’s protected
Section titled “What’s protected”Every one of these is covered by the rule above — never persisted, shown once.
| Content | Where it lives normally |
|---|---|
| Message text | text / link parts |
| Attachments (images, video, voice memos) | media parts — see single-use downloads below |
| App Clip cards | app_clip parts |
| iMessage app cards | imessage_app parts — see updating a card below |
| Poll option text | poll.options[].text — see adding poll options below |
| Mentions | mention, mention_range, mentions on a text part |
| Message edits | The new text from PATCH /v3/messages/{id} |
Example: sending a text message
Section titled “Example: sending a text message”{ "message": { "parts": [{ "type": "text", "value": "Your table is ready" }] }}{ "id": "b2c3d4e5-0000-0000-0000-000000000001", "parts": [{ "type": "text", "value": "Your table is ready" }]}{ "event_type": "message.delivered", "data": { "id": "b2c3d4e5-0000-0000-0000-000000000001", "parts": [], "zero_retention": true }}{ "id": "b2c3d4e5-0000-0000-0000-000000000001", "parts": []}Attachments are single-use downloads
Section titled “Attachments are single-use downloads”A zero-day-retention attachment’s url can be fetched exactly once — the file is deleted after the first successful download, regardless of whether the message was inbound or outbound. Download it as soon as you see it: the first GET on the URL returns 200 with the file’s bytes, exactly like any other attachment.
A second request to that same url — even seconds later — returns:
{ "success": false, "error": { "status": 404, "code": 2003, "message": "attachment not found" }}The attachment’s id, filename, mime_type, and size_bytes are metadata, not content — they remain queryable after the file itself is gone.
Inbound webhooks always carry the real content
Section titled “Inbound webhooks always carry the real content”An inbound zero-day-retention message, poll, or card update shows its real content in the webhook that first announces it — message.received, poll.received, an inbound poll.updated, an inbound message.edited. zero_retention is still true on that payload; it’s telling you the content will never be persisted, not that this specific payload is stripped.
{ "event_type": "poll.received", "data": { "poll": { "options": [ { "option_id": "...", "text": "Tacos" }, { "option_id": "...", "text": "Sushi" } ] }, "zero_retention": true }}A later GET /v3/messages/{id}/poll on that same poll returns "text": "" for both options.
What’s not protected
Section titled “What’s not protected”These are unaffected by zero-day retention — they persist and stay queryable normally.
| Why | |
|---|---|
Poll votes (poll.vote.added / .removed) |
A vote is a tally fact (which option, which voter), not authored content. Tallying across every voter’s independent request requires storing it. |
| Reaction type (❤️, 👍, a sticker, etc.) and which part it’s on | Not authored content — a fixed reaction type plus a part index. |
Attachment metadata (id, filename, mime_type, size_bytes) |
A reference, not the file’s bytes. |
That a message was edited (is_edited on the part) |
A state flag, not the edited text itself. |
Two behavior changes this introduces
Section titled “Two behavior changes this introduces”Two write paths need real content to build the message they send to the recipient’s device — content that, by the rule above, is never stored. Both are add-only, in-place-update operations on something already sent, which is why they’re affected and a fresh send is not.
Adding options to a poll
Section titled “Adding options to a poll”Adding an option to a poll normally only needs the new option’s text — the existing options’ text is already stored. On a zero-day-retention line it isn’t, so you must resend every existing option, in the order they were created, followed by the new one(s):
{ "options": [ { "text": "Tacos" }, { "text": "Sushi" }, { "text": "Pizza" } ]}Omitting an existing option returns:
{ "success": false, "error": { "status": 400, "code": 1005, "message": "zero-day-retention polls require resending every existing option plus at least one new option", "doc_url": "https://docs.linqapp.com/channel/imessage/error/codes/1xxx/1005/" }, "trace_id": "trace_abc123def456"}This only applies when the adding line is zero-day-retention — it’s evaluated per call, independent of whether the poll’s original creator was.
Updating an iMessage app card
Section titled “Updating an iMessage app card”Updating a card in place normally inherits the original card’s app identity (name, team_id, bundle_id) automatically. On a zero-day-retention line that identity was never stored, so:
- Using
experience— nothing changes. The identity always comes from the experience configuration, not the original card. - Using
urlorraw_payload_data(a raw update) — you must supplyappyourself:
{ "url": "https://example.com/updated-card", "layout": { "caption": "Updated" }, "app": { "name": "Your App", "team_id": "AB12CD34EF", "bundle_id": "com.example.app.MessagesExtension" }}Omitting app on a zero-day-retention raw update returns:
{ "success": false, "error": { "status": 400, "code": 1005, "message": "app identity is required: the original card was sent on a zero-day-retention line, so its app identity was never persisted — resupply it via `app`, or use `experience` instead", "doc_url": "https://docs.linqapp.com/channel/imessage/error/codes/1xxx/1005/" }, "trace_id": "trace_abc123def456"}Best practices
Section titled “Best practices”- Persist what you need, when you first see it. The synchronous response (outbound) or first webhook (inbound) is the only place real content ever appears.
- Don’t treat empty
partson a later webhook orGETas an error. For a zero-day-retention item, that’s the expected, correct response. - Download attachments immediately. A
urlthat works now may 404 moments later if it’s already been fetched once. - Track poll option text client-side if your line is zero-day-retention and you expect to add options later — you’re the only one holding a copy of it.
- Check
zero_retentionon the webhook, not the message ID or chat, to know how to interpret a given payload.
Related
Section titled “Related”- Polls — the full poll lifecycle
- iMessage Apps — cards and in-place updates
- Attachments — upload and download flow
- Mentions
- Webhook Events — every payload shape referenced above