---
title: Zero-Day Retention | API Docs
description: 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

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.

Capture content when you first see it

If your integration needs a zero-day-retention message’s text, an attachment’s bytes, a poll option’s text, or a card’s identity later, capture it yourself the first time it appears — that’s the only time Linq has it to hand back.

## 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](#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

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
  }
}
```

## 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](#attachments-are-single-use-downloads) below                           |
| App Clip cards                           | `app_clip` parts                                                                                                  |
| iMessage app cards                       | `imessage_app` parts — see [updating a card](#updating-an-imessage-app-card) below                                |
| Poll option text                         | `poll.options[].text` — see [adding poll options](#adding-options-to-a-poll) below                                |
| Mentions                                 | `mention`, `mention_range`, `mentions` on a text part                                                             |
| Message edits                            | The *new* text from [`PATCH /v3/messages/{id}`](/channel/imessage/api/resources/messages/methods/update/index.md) |

### Example: sending a text message

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": []
}
```

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

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

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.

## 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.                                                                                                        |

Note

A reaction on a zero-day-retention message’s part is still delivered live via its own `reaction.added` webhook with the real `part_index`. It just won’t appear nested under that part on a later `GET`, because the part itself has no content to nest it under.

## 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

[Adding an option](/channel/imessage/guides/messaging/polls#add-options/index.md) 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 an iMessage app card

[Updating a card in place](/channel/imessage/api/resources/messages/methods/update_app_card/index.md) 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"
}
```

## 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 `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.

## Related

- [Polls](/channel/imessage/guides/messaging/polls/index.md) — the full poll lifecycle
- [iMessage Apps](/channel/imessage/guides/messaging/imessage-apps/index.md) — cards and in-place updates
- [Attachments](/channel/imessage/guides/messaging/attachments/index.md) — upload and download flow
- [Mentions](/channel/imessage/guides/messaging/mentions/index.md)
- [Webhook Events](/channel/imessage/guides/webhooks/events/index.md) — every payload shape referenced above
