Skip to content

Handle inbound media from webhooks

Learn how to detect, fetch, and download photos, videos, documents, and stickers sent by WhatsApp users.

When a WhatsApp user sends an image, video, voice note, document, or sticker to your number, Wazapin alerts your server via a webhook.

Follow this guide to identify media messages, retrieve their download URLs, and download the media files to your server or cloud storage.

When this matters

Inbound media handling is essential for:

  • Saving customer-uploaded PDFs, receipts, or invoices.
  • Processing user-submitted photos or videos for customer support.
  • Archiving voice notes (audio messages) for transcription or QA.

Processing flow

Downloading user-submitted media involves three main steps:

graph TD
    A[User sends image/document] --> B[Webhook message.new msg_type: image/document]
    B --> C[GET /v1/messages/{id} to get media_url]
    C --> D[Fetch and stream media file to storage]
    D --> E[Acknowledge webhook 200 OK]

1. Detect media webhook

Wazapin POSTs a lightweight message.new webhook with a msg_type set to image, video, audio, document, or sticker.

Here is an example webhook payload for an inbound image:

{
  "message_id": "9f1fd66d-c37a-4b50-a8c2-b4dca523f9c8",
  "conversation_id": "0f89b0f9-74b4-44f9-b9b6-48f6d4de57aa",
  "contact_id": "c1a2b3c4-d5e6-7890-abcd-ef1234567890",
  "channel_id": "wzp_abc123",
  "direction": "inbound",
  "from_phone": "6281234567890",
  "msg_type": "image"
}

2. Fetch the media URL

Call GET /v1/messages/{messageID} using the message_id from the payload. The API response will contain the content block with a direct download link:

  • For images/videos/audio/documents: content.media_url
  • For stickers: content.sticker_url

3. Download the file

Stream the binary content from media_url (or sticker_url) to your local file system, AWS S3, Google Cloud Storage, or other storage provider.


Code example

Here is how to catch the webhook, get the message details, and download the file:

# 1. Fetch message details to get the media_url
curl -X GET "https://api.wazapin.com/v1/messages/9f1fd66d-c37a-4b50-a8c2-b4dca523f9c8" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Accept: application/json"

# 2. Download the binary file directly using the media_url from the response
curl -o "receipt.png" "https://api.wazapin.com/v1/media/assets/download_url"
import express from "express";
import { Webhook } from "svix";
import { WazapinClient } from "@wazapin/sdk";
import fs from "fs";
import axios from "axios";
import path from "path";

const app = express();
const wazapin = new WazapinClient({ apiKey: process.env.WAZAPIN_API_KEY });
const wh = new Webhook(process.env.WAZAPIN_WEBHOOK_SECRET!);

app.post("/webhooks/wazapin", express.raw({ type: "application/json" }), async (req, res) => {
  try {
    wh.verify(req.body, req.headers as Record<string, string>);
  } catch (err) {
    return res.status(403).send("Invalid signature");
  }

  const payload = JSON.parse(req.body.toString("utf8"));
  const mediaTypes = ["image", "video", "audio", "document", "sticker"];

  if (payload.direction === "inbound" && mediaTypes.includes(payload.msg_type)) {
    const messageId = payload.message_id;

    // 1. Retrieve the message record to get the media URL
    const { data: message } = await wazapin.messages.get(messageId);
    
    // Stickers use content.sticker_url; other media types use content.media_url
    const mediaUrl = message.type === "sticker" 
      ? message.content?.sticker_url 
      : message.content?.media_url;

    if (mediaUrl) {
      // 2. Download and save the file
      const downloadPath = path.join(__dirname, "downloads", `${messageId}.bin`);
      const response = await axios({
        url: mediaUrl,
        method: "GET",
        responseType: "stream",
      });

      const writer = fs.createWriteStream(downloadPath);
      response.data.pipe(writer);

      writer.on("finish", () => console.log(`Downloaded media to ${downloadPath}`));
      writer.on("error", (err) => console.error("Download failed", err));
    }
  }

  res.status(200).send("OK");
});
from fastapi import FastAPI, Request, HTTPException
from svix.webhooks import Webhook, WebhookVerificationError
import requests
import os

app = FastAPI()
wh = Webhook(os.environ["WAZAPIN_WEBHOOK_SECRET"])
API_KEY = os.environ["WAZAPIN_API_KEY"]

@app.post("/webhooks/wazapin")
async def handle_webhook(request: Request):
    body = await request.body()
    try:
        wh.verify(body, dict(request.headers))
    except WebhookVerificationError:
        raise HTTPException(status_code=403, detail="Invalid signature")

    payload = await request.json()
    media_types = {"image", "video", "audio", "document", "sticker"}

    if payload.get("direction") == "inbound" and payload.get("msg_type") in media_types:
        message_id = payload.get("message_id")
        
        # 1. Retrieve message details
        response = requests.get(
            f"https://api.wazapin.com/v1/messages/{message_id}",
            headers={"X-Api-Key": API_KEY, "Accept": "application/json"}
        )
        
        if response.status_code == 200:
            message_data = response.json().get("data", {})
            msg_type = message_data.get("type")
            content = message_data.get("content", {})
            
            # Stickers use sticker_url; other media types use media_url
            media_url = content.get("sticker_url") if msg_type == "sticker" else content.get("media_url")
            
            if media_url:
                # 2. Download and write file binary
                media_res = requests.get(media_url, stream=True)
                if media_res.status_code == 200:
                    file_path = f"downloads/{message_id}.bin"
                    with open(file_path, "wb") as f:
                        for chunk in media_res.iter_content(chunk_size=8192):
                            f.write(chunk)
                    print(f"Media successfully saved to {file_path}")

    return {"ok": True}

Channel support

Both official and unofficial channels support inbound media messages.

  • Official Channels: Handled via Meta’s secure media servers. Direct download URLs are proxy links provided by Wazapin and include expiring tokens.
  • Unofficial Channels: Pairs with WhatsApp Web to cache and upload media assets to Wazapin’s cloud store.

For details on supported media sizes and MIME types, see the Channel support matrix.


Troubleshooting

Expired media URLs

Wazapin media download URLs contain temporary access tokens. To prevent broken links, download the files immediately upon receiving the webhook. Do not store the media_url in your database expecting it to work indefinitely.

Wrong content-type or extensions

Wazapin proxy endpoints attempt to resolve correct MIME types and extensions. However, always validate the magic bytes or Content-Type headers when writing files to disk (e.g., verifying a .png file has png headers) to prevent execution vulnerabilities.

HTML landing page issues

If the downloaded file contains text resembling an HTML error page, check if the source URL has expired or if your API Key has permission to retrieve the message.


Next steps

Navigation

Type to search…

↑↓ navigate↵ selectEsc close