# Unfurling links

Roam apps can receive matching links from new messages and attach rich preview
cards without Roam fetching the destination. This is useful for incident,
ticket, document, and deployment links whose best summary is available only to
your app.

## 1. Register domains

In **Roam Administration → Developer → API Client → Webhooks**, add up to five
Link Unfurling Domains. Exact hosts match only themselves. A leading wildcard
matches exactly one additional label:

| Registration | Matches | Does not match |
|---|---|---|
| `status.example.com` | `status.example.com` | `www.status.example.com` |
| `*.example.com` | `status.example.com` | `example.com`, `a.status.example.com` |

Roam canonicalizes entries to lowercase ASCII hostnames. Schemes, paths, ports,
IP addresses, localhost, public suffixes, and misplaced wildcards are rejected.
Domain changes may take up to one minute to propagate in production.

Works with **API Key** and **OAuth** clients. Personal Access Token clients
cannot register unfurl domains (or call `chat.unfurl`). OAuth installations
inherit the domains configured on their parent OAuth app.

## 2. Subscribe to link events

Request `links:read`, configure a webhook URL, and subscribe to
[`chat.link.shared`](/docs/webhooks/chat-link-shared). Each delivery uses the
common webhook envelope. Its `data.links` list contains only URLs matching that
installation's registrations, uniquely and in message order.

The event fires for newly created text and markdown messages only. It does not
fire for edits or messages sent by that same app installation, preventing an
unfurl response loop.

## 3. Attach previews

Request `links:write` and call [`POST /chat.unfurl`](/docs/api/chat-unfurl) with
the event's `chatId`, `messageTimestamp`, and one or more exact URL keys:

```json
{
  "chatId": "8f3b9c2e-1a4d-4e7b-9c0a-2b6d1f5e3a7c",
  "messageTimestamp": 1748906400000000,
  "unfurls": {
    "https://status.example.com/incidents/123": {
      "title": "Incident 123",
      "description": "Investigating elevated errors",
      "siteName": "PagerDuty"
    }
  }
}
```

Every URL is checked against the message's current text and the app's current
domain registrations. If any URL fails, the whole request is rejected with
`cannot_unfurl_url` and no cards change. App cards replace Roam-generated cards
for the same exact URL, preserve unrelated previews, and appear in message-link
order. The latest valid app update wins.

`favicon` and `image.url` must be `https` display URLs. Roam's server does not
fetch them; clients render them with the existing link-preview card.

Successful unfurls currently persist as an ordinary message edit (version bump).
Clients may show an **Edited** label on the original message, and subscribers
with `chat:history` receive a `chat.message` edit event. Suppressing the Edited
badge for preview-only writes is planned as a follow-up.