Media Handling
Receive, download, and send media files through the Pipes.bot 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 delivers a download URL in the webhook payload. You can also send media back through the Messages API.
Receiving media
When a media message (image, audio, video, document, or sticker) arrives, Pipes.bot automatically:
- Downloads the file from WhatsApp's CDN
- Stores it in temporary storage with a 48-hour TTL
- Delivers the webhook with a
pipes.mediaobject containing the download URL
{
"object": "whatsapp_business_account",
"entry": [
{
"id": "pool_number_id",
"changes": [
{
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "+15551234567",
"phone_number_id": "pool_number_id"
},
"messages": [
{
"id": "msg_abc123",
"from": "15559876543",
"timestamp": "2025-01-15T10:30:00.000Z",
"type": "image",
"image": {
"id": "meta_media_id",
"mime_type": "image/jpeg",
"caption": "Check this out"
}
}
],
"contacts": [
{
"profile": { "name": "Jane Doe" },
"wa_id": "15559876543"
}
]
},
"field": "messages"
}
]
}
],
"pipes": {
"conversationId": "conv_xyz789",
"poolNumberId": "pool_number_id",
"media": {
"mediaId": "aBcDeFgHiJkLmNoPqRs1t",
"downloadUrl": "/v1/media/download/aBcDeFgHiJkLmNoPqRs1t",
"mimeType": "image/jpeg",
"byteSize": 245120
}
}
}Unavailable media
If Pipes.bot cannot download the file from WhatsApp (e.g. network error, expired CDN link), the message is still delivered with pipes.media.unavailable: true. The mediaId and downloadUrl fields will be absent.
{
"pipes": {
"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 webhook payload 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). The response includes Content-Type and Content-Disposition headers matching the original file.
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", // Follow the presigned URL redirect
});
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
Downloaded media is retained for 48 hours from receipt. After that, the file is automatically deleted from storage. If you need to keep the file longer, download and store it in your own storage within that window.
Expired media returns 410 Gone:
{ "error": "Media expired", "code": "MEDIA_EXPIRED" }Error responses
| 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 Messages API by providing a media source in the media field. Pipes.bot auto-detects the mode based on the value.
POST /v1/messages/send
Authorization: Bearer YOUR_API_KEY
Content-Type: application/jsonUsing a media ID
Reference a previously uploaded file by its media ID (the 21-character ID returned from the upload endpoint or from a received message). The file keeps its 48-hour TTL and can be reused across multiple sends.
{
"conversationId": "conv_xyz789",
"media": "aBcDeFgHiJkLmNoPqRs1t",
"caption": "Here's that photo"
}Using a URL
Provide an HTTPS URL and Pipes.bot fetches the file for you. The URL must point to a public server (private/internal addresses are blocked).
{
"conversationId": "conv_xyz789",
"media": "https://example.com/files/report.pdf",
"caption": "Monthly report"
}Using base64
Send file data inline as a base64-encoded string. 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"
}Sending media 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. This is useful when you want to reuse 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" }The returned mediaId can be used in the media field of the send endpoint. Uploaded media expires after 48 hours.
Supported media types
WhatsApp enforces format and size limits per media type. Pipes.bot validates these before uploading to WhatsApp.
| 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 media type (image, audio, video, sticker) are sent as documents.
Rate limits
The API enforces 60 requests per minute per API key across all endpoints. Rate limit headers are included in every response:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per window (60) |
X-RateLimit-Remaining | Requests remaining in this window |
X-RateLimit-Reset | Unix timestamp when window resets |
Exceeding the limit returns 429 Too Many Requests.
Complete example
A webhook handler that receives an image and sends it back with a watermark:
app.post("/webhook", async (req, res) => {
const payload = req.body;
if (payload.pipes?.test) {
return res.sendStatus(200);
}
const message = payload.entry[0].changes[0].value.messages[0];
const pipes = payload.pipes;
// Handle incoming image
if (message.type === "image" && pipes.media && !pipes.media.unavailable) {
// Download the original image
const imageBuffer = await downloadMedia(pipes.media.mediaId);
// Process the image (your logic here)
const processed = await addWatermark(imageBuffer);
// Send it back as base64
await fetch("https://api.pipes.bot/v1/messages/send", {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
conversationId: pipes.conversationId,
media: processed.toString("base64"),
mimeType: "image/jpeg",
caption: "Here's your watermarked image",
}),
});
}
res.sendStatus(200);
});