Most platforms put the burden of data retention entirely on the customer. We believe it should be part of the infrastructure. Today we are launching a new privacy tier on the Linq API built for the conversations that should never sit in storage: regulated workflows, PHI, financial data, identity verification, and any time you just want to keep data private. The tier ships with two independent features, ephemeral messages and ephemeral attachments, and it gives you something most messaging platforms make you engineer yourself: guaranteed content deletion on a fixed schedule, with zero content retention on our side. We keep the minimal pointers and deletion audit records compliance requires, and nothing you said. Turn it on for a single phone number, a set of numbers, or your whole partner account, and it applies automatically from that point forward.
Both features do the same thing at a high level. We keep the minimal pointers needed to support product behavior like reactions, and we delete your actual content. Message bodies, attachment bytes, formatting. All of it goes, on a fixed schedule, without you calling anything.
Why we built it
You can already delete content yourself. DELETE /v3/messages/{messageId} and DELETE /v3/attachments/{attachmentId} hard-delete the content from Linq storage. That works when every delete call fires and lands.
But networks are messy. You might miss an event, a transient error drops a request, and a message gets stuck and never cleaned up. Building zero retention on top of per-object delete calls means one dropped request leaves content behind.
The ephemeral tier removes that dependency. The platform enforces deletion on a fixed timer. You do not have to call delete on every message and every attachment to get to zero retention. If you never make a single delete call, the content still expires.
Ephemeral messages
Opt in by contacting your Linq support contact. When it is enabled, every message on the covered phone numbers gets a fixed 24-hour retention window. After that window, the platform permanently deletes the message from Linq storage. There is no per-message flag. Ephemerality is applied automatically based on your configuration.
The 24 hours run from message creation (created_at), which is the moment the content first hits our servers. Not from delivery, not from read. We store the content encrypted (AES-256 at rest, TLS 1.2+ in transit) for that window, then revoke it. This holds for both outbound and inbound messages on the covered numbers.
Two scopes are available:
| Scope | Effect |
|---|---|
| Partner-wide | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. |
| Per phone number | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. |
Here is how the ephemeral tier differs from the standard default:
| Aspect | Standard | Ephemeral |
|---|---|---|
| Retention | Retained per the standard message-retention policy | Hard backstop: 24 hours from when the message is created |
| After expiry | Message stays retrievable | Message is permanently deleted. GET /v3/messages/{messageId} returns 404 and it no longer appears in GET /v3/chats/{chatId}/messages |
| Content on expiry | N/A | Text, formatting, and attachment references are scrubbed. The message is gone, not blanked out |
| Cross-partner isolation | Enforced | Enforced |
A few properties of the window worth knowing:
- It is fixed at 24 hours from created_at and cannot be configured per message.
- It mirrors the ephemeral attachments 1-day backstop, so a message and any media it carries expire together.
- Expiry is delivery-independent. The clock starts at creation, not at delivery or read.
And a few things you will observe at the API level:
- No expiry timestamp is exposed. API responses and webhook payloads do not include the deletion time. If you need it, compute created_at + 24h yourself.
- No deletion webhook is sent. There is no message.deleted event. A message simply stops being retrievable once its window passes.
- Delivery is unaffected. Ephemeral messages send, deliver, and fire the usual message.sent, message.received and status webhooks exactly like standard messages. Only retention changes.
Important: Treat your application as the system of record. After 24 hours we cannot return the message to you. Not "will not," we cannot. If you need any part of it, persist it from the webhook payload at delivery.
// Capture message content at delivery time. After 24h, Linq cannot return it.
app.post("/webhooks/linq", (req, res) => {
if (!verifyWebhook(SIGNING_SECRET, req.rawBody, req.headers)) {
return res.status(401).end();
}
const event = JSON.parse(req.rawBody);
if (event.type === "message.sent" || event.type === "message.received") {
const msg = event.data;
// Persist anything you need to retain. Reading it back later will 404.
storeMessage({
messageId: msg.id,
chatId: msg.chat_id,
parts: msg.parts,
createdAt: msg.created_at,
});
}
res.status(200).end();
});
Verify the signature before you trust the payload. Webhooks are signed following the Standard Webhooks spec. The signed content is {webhook-id}.{webhook-timestamp}.{body}.
import * as crypto from "crypto";
function verifyWebhook(secret: string, rawBody: string, headers: Record<string, string>): boolean {
const msgId = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signature = headers["webhook-signature"];
const secretStr = secret.startsWith("whsec_") ? secret.slice(6) : secret;
const keyBytes = Buffer.from(secretStr, "base64");
const signedContent = `${msgId}.${timestamp}.${rawBody}`;
const expected = crypto.createHmac("sha256", keyBytes).update(signedContent).digest("base64");
return signature.split(" ").some((sig) => {
if (!sig.startsWith("v1,")) return false;
try {
return crypto.timingSafeEqual(
Buffer.from(expected, "base64"),
Buffer.from(sig.slice(3), "base64"),
);
} catch {
return false;
}
});
}
Ephemeral attachments
Attachments have their own tier because the retention problem for files is different from the one for text. Opt in the same way, through your Linq support contact, at the same two scopes.
Attachment URLs come in two layouts depending on the tier:
| Tier | URL pattern | TTL |
|---|---|---|
| Persistent (default) | https://cdn.linqapp.com/attachments/partners/{partner\_id}/{attachment\_id}/{filename} | Long-lived |
| Ephemeral | Pre-signed URL pointing at the ephemeral prefix on cdn.linqapp.com | 15 minutes per signed URL. Re-fetch via the API for a fresh URL |
Behavior against the persistent default:
| Aspect | Persistent | Ephemeral |
|---|---|---|
| Download URL form | Long-lived CDN URL | Pre-signed URL with short TTL |
| Retention floor | Indefinite (until you call DELETE) | Hard backstop: 1 day. Even without an explicit DELETE, the platform removes the underlying bytes after 24 hours |
| URL re-fetch | Not required | Fetch via GET /v3/attachments/{attachmentId} for a fresh signed URL after TTL expiry |
| Cross-partner isolation | Enforced | Enforced |
Outbound. Pre-signed URLs are generated and last 15 minutes. After you dispatch a message, the URL and its contents are removed from our system 15 minutes after they were created. Past that point the content is revoked. It is architecturally impossible for that URL to resolve.
Inbound. We host the received media and hand you a single-use, short-TTL signed URL. Use it once: download the file and host it on your end. If the TTL expires before you fetch, get a fresh signed URL via GET /v3/attachments/{attachmentId}. The bytes are removed from Linq servers on the 1-day backstop regardless.
func handleInboundMedia(ctx context.Context, part Part, attachmentID string) error {
// part.URL is the short-TTL signed URL from the webhook.
resp, err := http.Get(part.URL)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
// TTL expired. Fetch a fresh signed URL, then retry.
meta, err := client.Attachments.Retrieve(ctx, attachmentID)
if err != nil {
return err
}
resp, err = http.Get(meta.DownloadURL)
if err != nil {
return err
}
defer resp.Body.Close()
}
return storeBytesInYourOwnStorage(attachmentID, resp.Body)
}
Inbound media over webhooks uses the same URL layout your outbound sends produce, so the URL you store and the URL you build look identical. No special casing in your client. If the receiving phone is opted in to ephemeral, the url in the webhook is a short-TTL signed URL rather than a long-lived one.
If you want to remove an attachment before the backstop, DELETE still works and is irreversible:
DELETE /v3/attachments/{attachmentId}
Authorization: Bearer <your_api_key>
A 204 No Content means the bytes are gone. There is no undelete. The message part that referenced the attachment is preserved with no attachment reference, and any previously delivered webhook keeps its original URL string, but downloads from that URL return 404 going forward.
How the two tiers interact
The features are independent. You can run ephemeral attachments without ephemeral messages, or both together.
| Data | Persistent tier | Ephemeral tier |
|---|---|---|
| Attachment bytes | Retained until you DELETE | Auto-removed after 1 day, also removable via DELETE |
| Attachment metadata (id, filename, mime type, size) | Retained until you DELETE | Removed alongside the bytes |
| Message body and parts | Retained per message-retention policy | Retained per message-retention policy, unless the line also has ephemeral messages enabled, in which case the message and its parts are deleted 24 hours after creation |
| Audit log of deletions | Retained per platform retention policy | Retained per platform retention policy |
When to use it
We recommend using the ephemeral tier when:
- You have a compliance requirement that the platform must not retain content beyond a short window
- The conversation is high-sensitivity: PHI, financial, identity verification
- Your application is the system of record. You capture what you need from the delivery webhook in real time and do not read history back from Linq later
- You're running agents that generate message volume at a scale that prohibits a strategy of manual delete calls on every message
The design assumption is that you hold the durable copy. Linq delivers the message and the media, fires the webhook, and then gets out of the way. Anything you do not persist at delivery time is gone in 24 hours.
Enabling ephemeral messaging
You can toggle on ephemeral messaging partner-wide from your Linq dashboard’s settings.
Full reference:


