Skip to content
Linq Copy agent prompt

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.

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.

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.

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.

message.sent — zero-day-retention line
{
"event_type": "message.sent",
"data": {
"id": "8f14e45f-ceea-467e-adc0-000000000001",
"direction": "outbound",
"parts": [],
"zero_retention": true
}
}
message.sent — normal line
{
"event_type": "message.sent",
"data": {
"id": "8f14e45f-ceea-467e-adc0-000000000002",
"direction": "outbound",
"parts": [
{ "type": "text", "value": "Running 10 minutes late!" }
],
"zero_retention": false
}
}

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}
POST /v3/chats/{chatId}/messages — request
{
"message": { "parts": [{ "type": "text", "value": "Your table is ready" }] }
}
Synchronous response — shows the real text once
{
"id": "b2c3d4e5-0000-0000-0000-000000000001",
"parts": [{ "type": "text", "value": "Your table is ready" }]
}
message.delivered webhook — moments later
{
"event_type": "message.delivered",
"data": {
"id": "b2c3d4e5-0000-0000-0000-000000000001",
"parts": [],
"zero_retention": true
}
}
GET /v3/messages/{id} — anytime after
{
"id": "b2c3d4e5-0000-0000-0000-000000000001",
"parts": []
}

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:

Second GET on the same url — 404
{
"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.

poll.received — inbound, zero-day-retention line
{
"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.

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 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 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):

Poll already has "Tacos
{
"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 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 url or raw_payload_data (a raw update) — you must supply app yourself:
POST /v3/messages/{id}/update
{
"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"
}
  • 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 parts on a later webhook or GET as an error. For a zero-day-retention item, that’s the expected, correct response.
  • Download attachments immediately. A url that 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_retention on the webhook, not the message ID or chat, to know how to interpret a given payload.