Recevoir une photo ou un document du client et le télécharger

Oui — une photo, un PDF, un audio ou une vidéo arrive sur votre webhook (type image, document, audio, video) et se télécharge par GET /v1/media/{id} (redirection vers une URL valable 1 h) ; réception : 1 000 messages gratuits par mois, puis 1 crédit pour 20.

Recevoir une photo ou un document du client et le télécharger

Astuce
Oui — une photo, un PDF, un audio ou une vidéo arrive sur votre webhook (type image, document, audio, video) et se télécharge par GET /v1/media/{id} (redirection vers une URL valable 1 h) ; réception : 1 000 messages gratuits par mois, puis 1 crédit pour 20.

Scénario

Vous avez demandé une pièce : « Envoyez-moi la photo de votre reçu ». Le client l'envoie. Votre serveur reçoit un message message.inbound de type image (ou document), télécharge le fichier et le range avec le dossier.

Oui, vous pouvez recevoir photos et documents (1 000 messages reçus gratuits par mois, puis 1 crédit pour 20). Il n'y a pas de fenêtre à respecter pour recevoir : elle n'existe que pour répondre. Types de média reçus : image, audio, video, document, sticker.

Conversation

Vous    : Pour valider, envoyez-moi la photo de votre reçu.
Client  : (envoie une photo avec la légende « reçu de paiement »)
Webhook : message.inbound  type=image  text="reçu de paiement"
          media_id="1234…"  media_mime="image/jpeg"
          media_url="https://wa-api.geskap.com/v1/media/9d3e…"
Vous    : Bien reçu, merci Awa. Nous vérifions et revenons vers vous.

Code

Ce qui arrive sur votre webhook : le gestionnaire vérifie la signature puis appelle onInbound.

Node.js

// Express — receive the webhook, verify it, then react.
import express from "express";
import crypto from "node:crypto";

const app = express();
const SECRET = process.env.GESKAP_WEBHOOK_SECRET; // shown once when you register the webhook

// Keep the RAW bytes: the signature covers them, not a re-serialized JSON.
app.post("/webhooks/whatsapp", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const got = req.get("X-Camairetech-Signature") || "";
  if (got.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
    return res.sendStatus(401);
  }
  res.sendStatus(200); // answer fast: one attempt, 8 s timeout, no retry
  const { event, data } = JSON.parse(req.body.toString("utf8"));
  if (event === "message.inbound") onInbound(data); // your code
});

app.listen(3000);

PHP

<?php
// Receive the webhook, verify it, then react.
$raw = file_get_contents("php://input"); // raw bytes, before json_decode
$expected = "sha256=" . hash_hmac("sha256", $raw, getenv("GESKAP_WEBHOOK_SECRET"));
$got = $_SERVER["HTTP_X_CAMAIRETECH_SIGNATURE"] ?? "";
if (!hash_equals($expected, $got)) {
    http_response_code(401);
    exit;
}
http_response_code(200);
$payload = json_decode($raw, true);
if ($payload["event"] === "message.inbound") {
    on_inbound($payload["data"]); // your code
}

Python

# Flask — receive the webhook, verify it, then react.
import hashlib, hmac, json, os
from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["GESKAP_WEBHOOK_SECRET"].encode()

@app.post("/webhooks/whatsapp")
def whatsapp_webhook():
    raw = request.get_data()  # raw bytes, before any JSON parsing
    expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Camairetech-Signature", "")):
        abort(401)
    payload = json.loads(raw)
    if payload["event"] == "message.inbound":
        on_inbound(payload["data"])  # your code
    return "", 200

Dans onInbound, téléchargez le fichier (la clé API doit avoir le droit de lecture des messages) :

async function onInbound(data) {
  if (!["image", "document", "audio", "video", "sticker"].includes(data.type)) return;
  // data.message_id est l'identifiant de stockage ; data.wa_message_id (wamid.…) fonctionne aussi
  const res = await fetch(`https://wa-api.geskap.com/v1/media/${data.message_id}`, {
    headers: { Authorization: `Bearer ${process.env.GESKAP_API_KEY}` },
    redirect: "follow",                       // 302 vers une URL signée valable 1 h
  });
  if (res.status === 404) return markMissing(data);  // rien d'hébergé : voir le piège
  const bytes = Buffer.from(await res.arrayBuffer());
  await storage.save(`${data.from}/${data.message_id}`, bytes, data.media_mime); // chez vous
}

En ligne de commande : curl -L -H "Authorization: Bearer $GESKAP_API_KEY" https://wa-api.geskap.com/v1/media/<id> -o fichier. La légende que le client a écrite sous la photo est dans text. Accusez réception avec une réponse en texte libre.

Limites exactes

ÉlémentLimite
Types reçusimage, audio, video, document, sticker
TéléchargementGET /v1/media/{id} : id interne (message_id) ou wamid.…
URL signéeredirection 302, valable 1 h : téléchargez tout de suite
Rien d'hébergé404 « Aucun média hébergé pour ce message. »
Coûtrecevoir : 1 000 messages gratuits par mois, puis 1 crédit pour 20 (par défaut)
Attention
Piège : téléchargez et rangez chez vous sans attendre. L'URL signée expire au bout d'1 h, et le fichier n'est pas hébergé indéfiniment : media_url vaut null (et GET /v1/media/… répond 404) quand aucun fichier n'a été conservé (stockage non activé, ou quota de stockage du compte atteint). Dans ce cas il ne reste que media_id et les métadonnées. Voir Archiver messages et médias.
Attention
Piège : ne faites pas confiance au fichier parce qu'il vient de WhatsApp : contrôlez le type (media_mime) et la taille avant de l'ouvrir, et ne l'exécutez jamais. Une pièce d'identité reçue est une donnée sensible : stockez-la chiffrée, avec une durée de conservation.

Preuve — vérifié dans le code de l'API : public_api.py, webhooks.py.