Receiving & Downloading Media
How media files arrive in WebSocket messages and how to download them.
When someone sends a photo, voice note, video, document, or sticker to your pool number, Pipes.bot automatically downloads it from WhatsApp, stores it temporarily, and delivers a download URL alongside the message.
How media arrives
Media messages arrive as standard whatsapp_message events with an additional media object in the payload:
{
"type": "whatsapp_message",
"data": {
"messageId": "msg_abc123",
"conversationId": "conv_xyz789",
"poolNumberId": "pool_123",
"poolNumberPhoneNumber": "+15551234567",
"fromNumber": "+15559876543",
"fromName": "Jane Doe",
"timestamp": "2025-01-15T10:30:00.000Z",
"type": "image",
"body": "Check this out",
"text": "Check this out",
"media": {
"mediaId": "aBcDeFgHiJkLmNoPqRs1t",
"downloadUrl": "/v1/media/download/aBcDeFgHiJkLmNoPqRs1t",
"mimeType": "image/jpeg",
"byteSize": 245120
}
}
}The body and text fields contain the caption, if the sender included one.
Media fields
| Field | Type | Description |
|---|---|---|
mediaId | string? | Obfuscated media ID for downloading |
downloadUrl | string? | Relative URL path: /v1/media/download/{mediaId} |
mimeType | string | MIME type (e.g. image/jpeg, audio/ogg) |
fileName | string? | Original file name, mainly present for documents |
byteSize | number | File size in bytes |
unavailable | boolean? | true if the media could not be downloaded from WhatsApp |
Media types
The media object is present for these message types:
| Message type | Typical MIME types | Has caption |
|---|---|---|
image | image/jpeg, image/png | Yes |
audio | audio/ogg, audio/mp4, audio/mpeg | No |
video | video/mp4, video/3gpp | Yes |
document | Any (e.g. application/pdf) | Yes |
sticker | image/webp | No |
Other message types (text, location, contacts, reaction) do not include a media object.
Unavailable media
If Pipes.bot cannot download the file from WhatsApp (network error, expired CDN link), the message is still delivered so your application knows a media message was sent. The unavailable flag is set to true, and mediaId and downloadUrl are absent:
{
"media": {
"mimeType": "image/jpeg",
"byteSize": 0,
"unavailable": true
}
}Always check for this flag before attempting a download.
Downloading media
Use the mediaId from the message to fetch the file via the REST API:
GET /v1/media/download/{mediaId}
Authorization: Bearer YOUR_API_KEYThe endpoint validates your API key and tenant ownership, then redirects (302) to a short-lived presigned URL with a 5-minute expiry. Follow the redirect to download the file.
Node.js
async function downloadMedia(mediaId: string, apiKey: string): Promise<Buffer> {
const response = await fetch(
`https://api.pipes.bot/v1/media/download/${mediaId}`,
{
headers: { Authorization: `Bearer ${apiKey}` },
redirect: "follow",
},
);
if (!response.ok) {
throw new Error(`Download failed: ${response.status}`);
}
return Buffer.from(await response.arrayBuffer());
}Python
import requests
def download_media(media_id: str, api_key: str) -> bytes:
response = requests.get(
f"https://api.pipes.bot/v1/media/download/{media_id}",
headers={"Authorization": f"Bearer {api_key}"},
allow_redirects=True,
)
response.raise_for_status()
return response.contentcURL
curl -L \
-H "Authorization: Bearer YOUR_API_KEY" \
https://api.pipes.bot/v1/media/download/aBcDeFgHiJkLmNoPqRs1t \
-o photo.jpgMedia expiry
Media is retained for 48 hours from receipt. After that, the file is automatically deleted from storage. Download and store files in your own storage if you need them longer.
Expired media returns 410 Gone:
{ "error": "Media expired", "code": "MEDIA_EXPIRED" }Download errors
| Status | Code | Description |
|---|---|---|
401 | UNAUTHORIZED | Missing or invalid API key |
403 | FORBIDDEN | Media belongs to a different tenant |
404 | NOT_FOUND | Media ID does not exist |
410 | MEDIA_EXPIRED | Media has expired (48-hour TTL) |
Complete example
A handler that saves incoming images to disk:
import fs from "fs/promises";
import path from "path";
ws.on("message", async (raw) => {
const event = JSON.parse(raw.toString());
if (event.type !== "whatsapp_message") return;
const msg = event.data;
// Handle any media type
if (msg.media && !msg.media.unavailable) {
const buffer = await downloadMedia(msg.media.mediaId, API_KEY);
// Determine file extension from MIME type
const ext = msg.media.mimeType.split("/")[1]; // e.g. "jpeg", "pdf"
const fileName = msg.media.fileName || `${msg.media.mediaId}.${ext}`;
await fs.writeFile(path.join("./downloads", fileName), buffer);
console.log(`Saved ${msg.type} from ${msg.fromNumber}: ${fileName}`);
if (msg.body) {
console.log(`Caption: ${msg.body}`);
}
}
});