Pipes.bot

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:

  1. Downloads the file from WhatsApp's CDN
  2. Stores it in temporary storage with a 48-hour TTL
  3. Delivers the webhook with a pipes.media object containing the download URL
Webhook payload for an image message
{
  "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_KEY

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

Media 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

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)

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/json

Using 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

FieldTypeDescription
conversationIdstringRequired. Target conversation ID
textstring?Text content. Used as caption for media if caption is absent
mediastring?Media ID, HTTPS URL, or base64 data. Either text or media must be provided
mimeTypestring?MIME type. Required for base64 mode
fileNamestring?File name. Optional, used for base64 mode
captionstring?Media caption (max 1024 chars). Takes precedence over text

Response

{
  "success": true,
  "messageId": "wamid.abc123...",
  "conversationId": "conv_xyz789"
}

Sending media errors

StatusCodeDescription
400INVALID_REQUESTMissing required fields or invalid input
400INVALID_MEDIAUnsupported MIME type or file exceeds size limit
400INVALID_URLURL blocked by SSRF protection
400FETCH_FAILEDCould not fetch media from URL
403FORBIDDENConversation or media belongs to another tenant
404NOT_FOUNDConversation or media ID not found
410MEDIA_EXPIREDReferenced media has expired
500META_UPLOAD_FAILEDFailed to upload to WhatsApp
500SEND_FAILEDWhatsApp 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_KEY

Multipart 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.

TypeMax sizeAccepted formats
Image5 MBimage/jpeg, image/png
Audio16 MBaudio/aac, audio/mp4, audio/mpeg, audio/amr, audio/ogg
Video16 MBvideo/mp4, video/3gpp
Sticker500 KBimage/webp
Document100 MBAny 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:

HeaderDescription
X-RateLimit-LimitMaximum requests per window (60)
X-RateLimit-RemainingRequests remaining in this window
X-RateLimit-ResetUnix 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);
});

On this page