Skip to content

feat(whatsapp): send outbound files and attachments via Cloud API#537

Open
scopsy wants to merge 3 commits into
vercel:mainfrom
scopsy:cursor/whatsapp-outbound-files
Open

feat(whatsapp): send outbound files and attachments via Cloud API#537
scopsy wants to merge 3 commits into
vercel:mainfrom
scopsy:cursor/whatsapp-outbound-files

Conversation

@scopsy
Copy link
Copy Markdown

@scopsy scopsy commented May 19, 2026

Implement media upload, MIME mapping, caption fallbacks, and card+file sequencing.

Summary

The WhatsApp adapter previously ignored files and attachments on outbound post() calls (only text and interactive cards were sent). This PR implements full outbound media support via the WhatsApp Cloud API:

  1. Binary uploadPOST /{phoneNumberId}/mediamedia_id → typed media message
  2. Link passthrough — HTTPS Attachment.url sent directly (no upload)
  3. Multi-file — one WhatsApp message per file/attachment, sent sequentially
  4. Captions — markdown or card fallback text on the first media message when supported
  5. Card + files — media first, then interactive buttons (when applicable)

Packages: @chat-adapter/whatsapp (minor)


Supported inputs

Input Description
files: FileUpload[] Binary buffers/blobs with filename and optional mimeType. Always uploaded via /media.
attachments: Attachment[] Typed media (image | file | video | audio). Binary via data / fetchData, or HTTPS URL-only via url.

Both can be combined on { markdown }, { raw }, { ast }, or { card } postables. files are processed first, then attachments.

Examples

// PDF with caption
await thread.post({
  markdown: "Here's the report",
  files: [{ data: pdfBuffer, filename: "report.pdf", mimeType: "application/pdf" }],
});

// Multiple files (N sequential messages)
await thread.post({
  markdown: "Two files attached",
  files: [
    { data: buf1, filename: "a.pdf", mimeType: "application/pdf" },
    { data: buf2, filename: "b.png", mimeType: "image/png" },
  ],
});

// Card with buttons + image file
await thread.post({
  card: approvalCard,
  files: [{ data: proofBuffer, filename: "proof.png", mimeType: "image/png" }],
});

// Files only (no text)
await thread.post({
  markdown: "",
  files: [{ data: buffer, filename: "data.xlsx" }],
});

Behavior reference

Message flow (with media)

postMessage()
  ├─ files or attachments present?
  │    YES → postMessageWithMedia()
  │         ├─ Resolve text (card fallback OR markdown/raw/ast)
  │         ├─ Caption strategy (see below)
  │         ├─ For each file/attachment: upload (if binary) → sendMediaMessage()
  │         └─ Card present?
  │              ├─ interactive buttons → sendInteractiveMessage()
  │              └─ text-only card fallback → sendTextMessage() (if caption didn't already send text)
  │
  └─ NO → existing text / card-only path (unchanged)

Multi-file

WhatsApp allows one media object per API message. Multiple files or attachments in a single post() produce N sequential messages. The returned RawMessage is the last one sent (same convention as long-text chunking).

File index Caption
First Markdown / card fallback text (when caption rules allow)
2…N No caption

Caption placement

Condition Behavior
Text ≤ 1024 chars, first media is not audio, media supports captions Text sent as caption on first media message
Text > 1024 chars Separate text message first, then media with no captions
First media is audio Separate text message first (audio does not support captions), then audio
No text (markdown: "", files only) Media only, no caption

MIME type → WhatsApp message type

MIME WhatsApp type
image/jpeg, image/png image
Other image/* (e.g. GIF, WebP, SVG) document
video/mp4, video/3gpp video
audio/* audio
Everything else (PDF, XLSX, etc.) document

For Attachment without mimeType, the adapter uses attachment.type (image → image, file → document, etc.), then applies MIME rules when mimeType is set.

Size limits (pre-flight)

Throws ValidationError when binary size is known (before upload):

Type Limit
image 5 MB
audio 16 MB
video 16 MB
document 100 MB

URL-only attachments skip size validation unless attachment.size is provided.

Card + files

When both a card and files/attachments are present:

  1. Media message(s) first — caption uses cardToFallbackText(card) on the first media item
  2. Card message second:
    • Valid reply buttons (1–3) → interactive button message
    • Otherwise → text fallback message (skipped if text was already sent as a leading message)

Card image vs files (important)

How image is provided Result
<Image> child or card.imageUrl only (no files) No real image media. Card becomes interactive text or text fallback; image URL may appear as plain text in fallback.
files / attachments + card with buttons Real image message + separate interactive button message

To send a photo with buttons, pass the image via files or attachments, not only as a card image child.

Link passthrough

  • Attachment with only url (no data / fetchData) → { link: url } in the media payload
  • URL must be https://
  • No /media upload call

Binary resolution

Source Path
FileUpload.data toBuffer()uploadMedia(){ id }
Attachment.data / fetchData Same
Attachment.url only { link } passthrough

Out of scope (follow-ups)

  • Stickers (WebP encoding requirements)
  • Voice notes (voice vs audio distinction)
  • Media ID caching across posts (30-day expiry)
  • Interactive message image headers (card-embedded images without files)
  • Replay integration test mock extensions for /media
  • Edit/replace flows that include files

Test plan

Automated

  • pnpm --filter @chat-adapter/whatsapp test — 127 tests (10 new file-upload cases + MIME mapping table)
  • pnpm validate (run before merge)

Manual

Video

Area.mp4

Checklist

  • All commits are signed and verified
  • pnpm validate passes
  • Changeset added — .changeset/whatsapp-outbound-files.md
  • Documentation updated — packages/adapter-whatsapp/AGENTS.md, apps/docs/content/docs/files.mdx

Implement media upload, MIME mapping, caption fallbacks, and card+file
sequencing. Add shared PlatformName support for whatsapp buffer utilities.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 19, 2026

@scopsy is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

Comment thread packages/adapter-whatsapp/src/index.ts Outdated
scopsy added 2 commits May 19, 2026 19:02
Implement outbound file and attachment sending for the WhatsApp adapter and update docs. Changes include: adding WhatsApp file/attachment handling in the adapter (binary uploads, sequential multi-file sends, caption rules, card+file sequencing), introducing a typed WHATSAPP_BUFFER_PLATFORM constant to pass to toBuffer, and minor code flow/comments around interactive cards and text messages. Documentation updated with a WhatsApp file uploads section and removed an earlier WhatsApp note from the shared files guide. Also remove WhatsApp from the shared PlatformName/button-style mapping so the adapter asserts the platform label where needed.
Fix logic that caused duplicate text to be sent when posting a card with an attached file by changing the conditional to only skip sending text when the text is empty. Add a unit test that posts a card with a file and verifies only one media upload and one message are sent and that the media caption contains the card title. This ensures the adapter does not send duplicate textual fallbacks alongside media.
@scopsy scopsy marked this pull request as ready for review May 19, 2026 16:06
@scopsy scopsy requested a review from a team as a code owner May 19, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant