Pipes.bot

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

FieldTypeDescription
mediaIdstring?Obfuscated media ID for downloading
downloadUrlstring?Relative URL path: /v1/media/download/{mediaId}
mimeTypestringMIME type (e.g. image/jpeg, audio/ogg)
fileNamestring?Original file name, mainly present for documents
byteSizenumberFile size in bytes
unavailableboolean?true if the media could not be downloaded from WhatsApp

Media types

The media object is present for these message types:

Message typeTypical MIME typesHas caption
imageimage/jpeg, image/pngYes
audioaudio/ogg, audio/mp4, audio/mpegNo
videovideo/mp4, video/3gppYes
documentAny (e.g. application/pdf)Yes
stickerimage/webpNo

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_KEY

The 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.content

cURL

curl -L \
  -H "Authorization: Bearer YOUR_API_KEY" \
  https://api.pipes.bot/v1/media/download/aBcDeFgHiJkLmNoPqRs1t \
  -o photo.jpg

Media 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

StatusCodeDescription
401UNAUTHORIZEDMissing or invalid API key
403FORBIDDENMedia belongs to a different tenant
404NOT_FOUNDMedia ID does not exist
410MEDIA_EXPIREDMedia 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}`);
    }
  }
});

On this page