OdyDocs

Hosted filesnew

Read or change how a workspace texts photos, videos and documents as a branded link.ody.co/m/… page (every attachment, every send, no per-send choice), and read opens from the activity and the media.* webhooks. On by default.

Hosted files turns every attachment into a page. When a message goes out with a photo, a gallery, a video, a PDF — one file or twelve, small or large — Ody puts those files on one branded page at link.ody.co/m/<token> — the business's name, logo, accent colour and a tracked Visit website button on top, the content opened on first paint (a gallery shows its first photo large with the rest in a strip), Ody only as a small footer line — and sends one text: your body, then 3 photos: link.ody.co/m/…. The recipient's opens, photo views and saves come back on the activity, in the thread and Analytics of the app, and as the media.opened / media.downloaded webhooks.

On by default (since 2026-11-07). It is a workspace preference — the same card the app shows under Settings → Analytics → Hosted files — and this page is how you read or change it from code. There is deliberately no per-send choice: with the switch on every attachment of every send is a page; PATCH { "enabled": false } restores the previous behaviour exactly: MMS when it fits, a bare https://api.ody.co/m/<token> secure link per oversized file when it doesn't, no page, no events.

What you get back elsewhere
  • On every outbound activity that carried a page: hostedMedia[] (title, layout, item count, opens, viewed items, saves, website taps, expiry, revocation) and media[].hostedUrl / media[].hostedMediaId on the attachments that live on it.
  • Every human open fires media.opened; a Save fires media.downloaded. Previewers and scanners never emit.
  • The page's Visit website button is an ordinary tracked link (source = media_page), so its taps arrive as link.clicked too.
  • In the app: a glass pill in the thread ("Maria opened your 3 photos · looked at all 3 · saved 1"), the conversation lifted to the top of the inbox on the first open, and a Files block on the Analytics screen.

#The settings object

  • enabled boolean — Enable hosted files. Every photo, video, PDF or file the team (or your integration) attaches arrives as a page. Default true; only a stored false turns it off, and then sending is exactly the pre-hosted-files behaviour (normal picture messages when they fit, a bare secure link when they don't).
  • displayName string | null — Business name shown on the page. null = the workspace name. ≤ 60 characters.
  • logoKey string | null — Storage key of the uploaded logo. Upload it in the app (Settings → Analytics → Hosted files → Upload logo); from the API you can only clear it with null.
  • accent string | null — #rrggbb accent colour for the counter, buttons and dots. null = Ody neutral. The page picks black or white ink on top of it automatically.
  • websiteUrl string | null — https:// URL. Adds a Visit website button whose taps are tracked (utm_medium=hosted_media, utm_campaign=ody_media_page, the contact reference as utm_content). null removes the button.
  • expiryDays integer — 0 (default) = the page stays open as long as the conversation exists; 7, 30 or 90 days to expire it sooner. Pages can also be revoked from the thread at any time.
  • allowSave object — { photos, documents, video } booleans. Shows a Save button for that kind and a downloadUrl in the page's manifest. Default photos + documents true, video false.
  • trackOpens boolean — Record opens / views / saves, show them in the thread and Analytics, and emit the media.* webhooks. Default true. Off keeps hosting the pages and records nothing.

Every response also carries baseUrl (https://link.ody.co/m).

Branding is snapshotted onto each page when the text is sent, so changing the logo or accent later never changes a page a customer already has.

#Read the settings

GET/v1/api/hosted-media

Scope: workspace:manage

curl https://api.ody.co/v1/api/hosted-media \
  -H "Authorization: Bearer $ODY_API_KEY"
Response
{
  "settings": {
    "enabled": true,
    "displayName": null,
    "logoKey": "workspaces/8f93…/branding/logo-1762550400000.png",
    "accent": "#1c6bff",
    "websiteUrl": "https://glowsalon.com",
    "expiryDays": 0,
    "allowSave": { "photos": true, "documents": true, "video": false },
    "trackOpens": true
  },
  "baseUrl": "https://link.ody.co/m"
}

#Switch it off or on, brand the page, change expiry and saving

PATCH/v1/api/hosted-media

Scope: workspace:manage

Send only the fields you want to change. Changes apply to the next text; pages already sent keep their branding and keep working.

  • enabled boolean
  • displayName string | null
  • logoKey null — Only null (clear) is accepted from the API; upload a new logo in the app.
  • accent string | null
  • websiteUrl string | null
  • expiryDays integer — 0 (always) · 7 · 30 · 90
  • allowSave object — any subset of { photos, documents, video }
  • trackOpens boolean

Errors — 400 invalid_argument when a field has the wrong type or value; the message is the sentence the app shows under the field (Use a hex colour like #1c6bff, Enter a full website address starting with https://, Pages can stay open 7, 30, 90 or 0 days, Upload the logo from the app, Keep the name under 60 characters). 403 permission_denied when the key lacks workspace:manage.

curl -X PATCH https://api.ody.co/v1/api/hosted-media \
  -H "Authorization: Bearer $ODY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "displayName": "Glow Salon", "accent": "#1c6bff", "websiteUrl": "https://glowsalon.com", "expiryDays": 90 }'
Response
{
  "settings": {
    "enabled": true,
    "displayName": "Glow Salon",
    "logoKey": null,
    "accent": "#1c6bff",
    "websiteUrl": "https://glowsalon.com",
    "expiryDays": 90,
    "allowSave": { "photos": true, "documents": true, "video": false },
    "trackOpens": true
  },
  "baseUrl": "https://link.ody.co/m"
}

#Every send is a page — there is no per-send flag

With Hosted files on, POST /messages puts every attachment of the send on one page — a single small photo that would have fit as MMS exactly like a 40 MB video. You do not (and cannot) ask for it per message: the sendAsLink flag that existed for a few hours on 2026-11-07 is ignored if sent, and the galleries setting is gone. A workspace that wants plain picture messages turns the switch off (PATCH { "enabled": false }); nothing else changes.

JSON
{ "conversationId": "c4f1…", "body": "Here is the quote", "mediaUrls": ["https://files.example.com/quote.pdf"] }

#What is on the page, and what is recorded

On the pageRecorded (human only)
A gallery opened on photo 1, strip + 1 / 7 counter, swipe / arrow keys / tap-to-jump, pinch to zoomopen (page), view per photo that settles in the hero
A video or audio clip playing inlineview on play, progress at 25 / 50 / 75 / 100 %
A document card with Open (native viewer) and Saveview on Open, download on Save
Visit website (when set)a link.clicked on a media_page tracked link
Text back — opens the recipient's messaging app on the line that sent the text—
Footer: Shared securely with Ody · Available until … · Report—

Previewers and scanners (iMessage, WhatsApp, Slack, security gateways) receive a generic card — "Glow Salon sent you 3 photos" — never the content, and nothing is recorded.

#Security

  • The URL token is 128-bit random (22 base64url characters) and stored only as a SHA-256 hash; unknown and malformed tokens get the same 404. Expired and revoked pages return 410.
  • Content is served from 5-minute signed URLs that the page refreshes while open; nothing is cached (Cache-Control: no-store), indexed (X-Robots-Tag: noindex) or referrable (Referrer-Policy: no-referrer).
  • The page ships a strict, nonce-based Content-Security-Policy, no cookies and no third-party scripts.
  • Photos are re-encoded with location and camera metadata stripped; every file is sniffed by content before it is accepted.
  • The open record keeps the device class and the country when the edge supplies it, and a daily-salted hash of the IP — never the IP itself.

#MCP

The same two operations are MCP tools: get_hosted_files and configure_hosted_files, gated by the same workspace:manage scope. send_message carries attachments the same way POST /messages does.

Last updated Sat Nov 07 2026 00:00:00 GMT+0000 (Coordinated Universal Time)Something wrong on this page? Tell us