Skip to content
Start free

Media messages

Wabery uses one message API and one normalized inbound webhook contract across WhatsApp Business, Instagram Direct, and Facebook Messenger.

The SDK and REST examples below are equivalent. Wabery resolves Meta’s channel-specific sender identifiers and credentials server-side, so integrations must pass Wabery channelId/channel_id values rather than Facebook Page or Instagram account IDs.

Operation REST SDK
Send media POST /v1/messages wabery.messages.send()
Check send status GET /v1/messages/{message_id} wabery.messages.get()
Read history or refresh a media URL GET /v1/conversations/{id}/messages wabery.conversations.listMessages()
Receive media message.received webhook wabery.webhooks.constructEvent()

The complete machine-readable contract is also available as OpenAPI.

There is no multipart upload endpoint. Send a public HTTPS URL that Meta can download, together with the existing channel_id and conversation_id. Instagram and Messenger are reply channels: the customer must have messaged that same channel during the previous 24 hours.

The PHP and Python examples below use the reusable clients from PHP, Python, and API clients.

await wabery.messages.send({
channelId: "instagram_channel_...",
conversationId: "conversation_...",
idempotencyKey: "reply-media-123",
media: {
type: "image",
link: "https://cdn.example.com/photo.jpg",
},
});

202 Accepted means Wabery queued the send; it does not mean Meta delivered it. Poll wabery.messages.get(message.id) or consume message.status webhooks to observe sent, delivered, read, or failed.

Channel Outbound types Provider limit
WhatsApp image, document, audio, video image 5 MB; audio/video 16 MB; document 100 MB
Instagram beta image, audio, video image 8 MB; audio/video 25 MB
Messenger beta image, document, audio, video 25 MB per attachment

WhatsApp accepts either a public HTTPS media.link or a previously uploaded WhatsApp media.id. It supports caption and document filename; audio accepts neither.

Instagram and Messenger accept media.link only. The URL must be fetchable by Meta without custom headers. Wabery rejects media.id, caption, and filename on those channels because Meta’s URL-attachment request cannot honor those fields. Instagram does not accept outbound documents; send the document URL as an ordinary text message instead.

Unsupported combinations fail before queueing:

Error Meaning
media_id_unsupported_on_channel Instagram/Messenger received media.id
public_https_media_link_required The channel requires a directly fetchable HTTPS URL
media_type_unsupported_on_channel The selected channel does not support that type
media_metadata_unsupported_on_channel The channel cannot honor caption or filename
messaging_window_closed No customer message on that channel within 24 hours

Media capability errors use HTTP 422. A closed messaging window uses HTTP 409. None of these responses queue a message.

Instagram, Messenger, and WhatsApp use the same normalized message.received shape. Provider-specific temporary URLs do not leak into your integration; Wabery caches the attachment and supplies a signed download URL.

{
"event": "message.received",
"api_version": "2026-06-18",
"payload": {
"channel_id": "instagram_channel_...",
"conversation_id": "conversation_...",
"contact_id": "contact_...",
"from": null,
"user_id": "INSTAGRAM_SCOPED_USER_ID",
"message_id": "message_...",
"type": "image",
"text": null,
"messages": [
{
"id": "message_...",
"type": "image",
"text": null,
"received_at": "2026-07-30T15:27:01.778Z",
"media": {
"id": "asset_...",
"message_id": "message_...",
"type": "image",
"url": "https://storage.wabery.com/assets/inbound/asset_...?token=...",
"expires_at": "2026-08-06T15:27:01.778Z",
"mime_type": "image/jpeg",
"file_name": "photo.jpg",
"file_size": 248913,
"provider": "instagram",
"provider_media_id": null,
"status": "available",
"error_code": null
},
"location": null,
"interactive": null,
"replied_to": null
}
],
"assets": [
{
"id": "asset_...",
"message_id": "message_...",
"type": "image",
"url": "https://storage.wabery.com/assets/inbound/asset_...?token=...",
"expires_at": "2026-08-06T15:27:01.778Z",
"mime_type": "image/jpeg",
"file_name": "photo.jpg",
"file_size": 248913,
"provider": "instagram",
"provider_media_id": null,
"status": "available",
"error_code": null
}
],
"media": {
"id": "asset_...",
"url": "https://storage.wabery.com/assets/inbound/asset_...?token=...",
"provider": "instagram",
"status": "available"
}
},
"sentAt": "2026-07-30T15:27:04.000Z"
}

Messenger uses the identical fields with provider: "messenger". A received PDF has type: "document" and normally mime_type: "application/pdf". provider_media_id may be null when Meta supplies only a temporary source URL.

file_name preserves the provider filename when Meta supplies it. Otherwise Wabery uses the response Content-Disposition, the source URL basename, or a generated safe name. Do not assume that the sender’s original local filename is always available.

On WhatsApp, from is the sender’s E.164 number. On Instagram and Messenger it is null; use user_id for the Instagram-scoped or Page-scoped sender ID.

The normalized media.url contains its authorization in the token query parameter. Download it directly—do not send the Wabery API key or an Authorization header.

  • The signed URL lasts one hour by default.
  • The underlying asset is retained for 24 hours on free plans and seven days on paid plans.
  • expires_at is the asset-retention deadline, not the current URL-token deadline.
  • Call wabery.conversations.listMessages(conversationId) to obtain a freshly signed URL while the asset remains retained.
  • Download and store files promptly, and check status before using url.
if (event.event === "message.received") {
for (const asset of event.payload.assets ?? []) {
if (asset.status !== "available" || !asset.url) continue;
const response = await fetch(asset.url);
if (!response.ok) throw new Error(`Failed to download ${asset.id}`);
await storeFile({
id: asset.id,
fileName: asset.file_name,
contentType: asset.mime_type,
bytes: await response.arrayBuffer(),
retainedUntil: asset.expires_at,
});
}
}

See Webhooks & events for signature verification and delivery retries, and Conversations & history for pagination and URL refresh.