Media Handling
Receive, download, and send media files through WebSocket and the REST API.
Pipes.bot handles the full media lifecycle for WhatsApp messages. When someone sends a photo, voice note, or document to your pool number, Pipes.bot downloads it from WhatsApp, stores it temporarily, and includes a download URL in the WebSocket message.
Receiving media
When a media message arrives (image, audio, video, document, or sticker), the WebSocket event includes a media object:
{
"type": "whatsapp_message",
"data": {
"type": "image",
"body": "Check this out",
"media": {
"mediaId": "aBcDeFgHiJkLmNoPqRs1t",
"downloadUrl": "/v1/media/download/aBcDeFgHiJkLmNoPqRs1t",
"mimeType": "image/jpeg",
"byteSize": 245120
}
}
}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 |
Unavailable media
If Pipes.bot cannot download the file from WhatsApp (network error, expired CDN link), the message is still delivered with unavailable: true. The mediaId and downloadUrl fields will be absent.
{
"media": {
"mimeType": "image/jpeg",
"byteSize": 0,
"unavailable": true
}
}Always check for this flag before attempting a download.
Downloading media
Use the media ID from the WebSocket message to download the file:
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 (5-minute expiry).
Node.js example
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 example
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.contentMedia expiry
Media is retained for 48 hours from receipt. After that, the file is automatically deleted. 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) |
Sending media
Send media messages through the REST API. The media field auto-detects the input mode.
POST /v1/messages/send
Authorization: Bearer YOUR_API_KEY
Content-Type: application/jsonUsing a media ID
Reference a previously received or uploaded file by its 21-character media ID:
{
"conversationId": "conv_xyz789",
"media": "aBcDeFgHiJkLmNoPqRs1t",
"caption": "Here's that photo"
}Using a URL
Provide an HTTPS URL and Pipes.bot fetches the file. Private/internal addresses are blocked.
{
"conversationId": "conv_xyz789",
"media": "https://example.com/files/report.pdf",
"caption": "Monthly report"
}Using base64
Send file data inline. The mimeType field is required.
{
"conversationId": "conv_xyz789",
"media": "iVBORw0KGgoAAAANSUhEUg...",
"mimeType": "image/png",
"fileName": "screenshot.png",
"caption": "See attached"
}Request fields
| Field | Type | Description |
|---|---|---|
conversationId | string | Required. Target conversation ID |
text | string? | Text content. Used as caption for media if caption is absent |
media | string? | Media ID, HTTPS URL, or base64 data. Either text or media must be provided |
mimeType | string? | MIME type. Required for base64 mode |
fileName | string? | File name. Optional, used for base64 mode |
caption | string? | Media caption (max 1024 chars). Takes precedence over text |
Response
{
"success": true,
"messageId": "wamid.abc123...",
"conversationId": "conv_xyz789"
}Send errors
| Status | Code | Description |
|---|---|---|
400 | INVALID_REQUEST | Missing required fields or invalid input |
400 | INVALID_MEDIA | Unsupported MIME type or file exceeds size limit |
400 | INVALID_URL | URL blocked by SSRF protection |
400 | FETCH_FAILED | Could not fetch media from URL |
403 | FORBIDDEN | Conversation or media belongs to another tenant |
404 | NOT_FOUND | Conversation or media ID not found |
410 | MEDIA_EXPIRED | Referenced media has expired |
500 | META_UPLOAD_FAILED | Failed to upload to WhatsApp |
500 | SEND_FAILED | WhatsApp rejected the message |
Uploading media
Upload media independently before sending. Useful for reusing the same file across multiple messages.
POST /v1/media/upload
Authorization: Bearer YOUR_API_KEYMultipart file upload
curl -X POST https://api.pipes.bot/v1/media/upload \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@photo.jpg"JSON with URL
curl -X POST https://api.pipes.bot/v1/media/upload \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/files/photo.jpg" }'JSON with base64
curl -X POST https://api.pipes.bot/v1/media/upload \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "data": "iVBORw0KGgo...", "mimeType": "image/png", "fileName": "chart.png" }'Response
{ "mediaId": "aBcDeFgHiJkLmNoPqRs1t" }Uploaded media expires after 48 hours.
Supported media types
WhatsApp enforces format and size limits per media type:
| Type | Max size | Accepted formats |
|---|---|---|
| Image | 5 MB | image/jpeg, image/png |
| Audio | 16 MB | audio/aac, audio/mp4, audio/mpeg, audio/amr, audio/ogg |
| Video | 16 MB | video/mp4, video/3gpp |
| Sticker | 500 KB | image/webp |
| Document | 100 MB | Any MIME type |
Files that don't match a specific type are sent as documents.
Complete example
A WebSocket handler that receives an image, processes it, and sends it back:
ws.on("message", async (raw) => {
const event = JSON.parse(raw.toString());
if (event.type !== "whatsapp_message") return;
const msg = event.data;
if (msg.type === "image" && msg.media && !msg.media.unavailable) {
// Download the original image
const imageBuffer = await downloadMedia(msg.media.mediaId, API_KEY);
// Process the image (your logic)
const processed = await addWatermark(imageBuffer);
// Send it back via REST API
await fetch("https://api.pipes.bot/v1/messages/send", {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
conversationId: msg.conversationId,
media: processed.toString("base64"),
mimeType: "image/jpeg",
caption: "Here's your watermarked image",
}),
});
}
});