Les messages reçus sont stockés côté API pour la durée de votre plan puis purgés ; pour les garder, copiez chaque message.inbound et téléchargez le média tout de suite. Les messages que vous envoyez par l'API ne sont pas dans GET /v1/messages/inbound.
Astuce
Les messages reçus sont stockés côté API pour la durée de votre plan puis purgés ; pour les garder, copiez chaque message.inbound et téléchargez le média tout de suite. Les messages que vous envoyez par l'API ne sont pas dans GET /v1/messages/inbound.
Un client vous envoie la photo d'un bon de commande. Vous voulez garder le message et la photo pour de bon : dans votre base, pour un litige ou un audit. Ou un client conteste un message reçu il y a trois mois. L'API garde une copie, mais pas indéfiniment. Pour conserver au-delà, vous devez copier.
Chaque message reçu est stocké automatiquement, même si vous n'avez pas de webhook. Vous le relisez avec GET /v1/messages/inbound. Le stockage dure selon votre plan de stockage ; passé ce délai, une purge quotidienne supprime définitivement les messages (et leurs médias hébergés).
curl
curl -X GET "https://wa-api.geskap.com/v1/storage" \
-H "Authorization: Bearer $GESKAP_API_KEY"
Node.js
const res = await fetch("https://wa-api.geskap.com/v1/storage", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.GESKAP_API_KEY}`,
},
});
console.log(res.status, await res.json());
PHP
$ch = curl_init("https://wa-api.geskap.com/v1/storage");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("GESKAP_API_KEY"),
],
]);
$res = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $res;
Python
import os, requests
res = requests.request(
"GET",
"https://wa-api.geskap.com/v1/storage",
headers={"Authorization": f"Bearer {os.environ['GESKAP_API_KEY']}"},
timeout=15,
)
print(res.status_code, res.json())
La réponse donne retention_days, le nombre de messages, la place utilisée et le plus ancien message conservé. Les plans et leurs durées sont des réglages : lisez-les dans GET /v1/public/pricing (rubrique plans de stockage) plutôt que de les écrire en dur.
message.inbound sur votre webhook
│
├─► écrire le message dans votre base (clé : wa_message_id)
└─► si media_url n'est pas null : télécharger le fichier maintenant dans votre stockage
Rattrapez les trous (panne de votre serveur) avec GET /v1/messages/inbound?since=<dernier received_at> : avec since, les messages reviennent du plus ancien au plus récent, strictement après ce curseur. Avancez le curseur au received_at du dernier message traité. Le paramètre limit va de 1 à 200 (50 par défaut).
Node.js
// Copy a received message, then its media, into YOUR storage.
import fs from "node:fs/promises";
const API = "https://wa-api.geskap.com";
const auth = { Authorization: `Bearer ${process.env.GESKAP_API_KEY}` };
export async function archive(msg) { // msg = webhook data, or one item of GET /v1/messages/inbound
await saveMessage(msg); // your DB, unique on msg.wa_message_id
if (!msg.media_url) return;
// The API answers 302 to a signed address valid 1 h: do not send your key to it.
const hop = await fetch(msg.media_url, { headers: auth, redirect: "manual" });
const signed = hop.headers.get("location");
if (!signed) return; // nothing hosted for this message
const file = await fetch(signed);
await fs.writeFile(`archive/${msg.wa_message_id}`, Buffer.from(await file.arrayBuffer()));
}
// Catch-up after an outage: oldest first, strictly after the cursor.
export async function catchUp(cursor) {
for (;;) {
const res = await fetch(`${API}/v1/messages/inbound?since=${encodeURIComponent(cursor)}&limit=200`, { headers: auth });
const { messages } = await res.json();
if (!messages.length) return cursor;
for (const m of messages) await archive(m);
cursor = messages[messages.length - 1].received_at;
}
}
PHP
<?php
// Copy a received message, then its media, into YOUR storage.
const API = "https://wa-api.geskap.com";
function api_get(string $url, bool $follow = false) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => $follow,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("GESKAP_API_KEY")],
]);
$body = curl_exec($ch);
$redirect = curl_getinfo($ch, CURLINFO_REDIRECT_URL);
return [$body, $redirect];
}
function archive(array $msg): void { // $msg = webhook data, or one item of GET /v1/messages/inbound
save_message($msg); // your DB, unique on wa_message_id
if (empty($msg["media_url"])) return;
// The API answers 302 to a signed address valid 1 h: do not send your key to it.
[, $signed] = api_get($msg["media_url"]);
if (!$signed) return; // nothing hosted for this message
file_put_contents("archive/" . $msg["wa_message_id"], file_get_contents($signed));
}
// Catch-up after an outage: oldest first, strictly after the cursor.
function catch_up(string $cursor): string {
while (true) {
[$body] = api_get(API . "/v1/messages/inbound?since=" . urlencode($cursor) . "&limit=200");
$messages = json_decode($body, true)["messages"];
if (!$messages) return $cursor;
foreach ($messages as $m) archive($m);
$cursor = end($messages)["received_at"];
}
}
Python
# Copy a received message, then its media, into YOUR storage.
import os, requests
API = "https://wa-api.geskap.com"
AUTH = {"Authorization": f"Bearer {os.environ['GESKAP_API_KEY']}"}
def archive(msg): # msg = webhook data, or one item of GET /v1/messages/inbound
save_message(msg) # your DB, unique on msg["wa_message_id"]
if not msg.get("media_url"):
return
# The API answers 302 to a signed address valid 1 h: do not send your key to it.
hop = requests.get(msg["media_url"], headers=AUTH, allow_redirects=False, timeout=15)
signed = hop.headers.get("Location")
if not signed:
return # nothing hosted for this message
with open(f"archive/{msg['wa_message_id']}", "wb") as f:
f.write(requests.get(signed, timeout=60).content)
def catch_up(cursor):
"""After an outage: oldest first, strictly after the cursor."""
while True:
r = requests.get(f"{API}/v1/messages/inbound", headers=AUTH,
params={"since": cursor, "limit": 200}, timeout=15)
messages = r.json()["messages"]
if not messages:
return cursor
for m in messages:
archive(m)
cursor = messages[-1]["received_at"]
curl
# 1) Read messages after a cursor (oldest first).
curl "https://wa-api.geskap.com/v1/messages/inbound?since=2026-10-30T09:00:00%2B00:00&limit=200" \
-H "Authorization: Bearer $GESKAP_API_KEY"
# 2) Get the signed address of a media (302, do not follow with your key).
SIGNED=$(curl -s -o /dev/null -w '%{redirect_url}' \
"https://wa-api.geskap.com/v1/media/9d3e0000-0000-0000-0000-000000000000" \
-H "Authorization: Bearer $GESKAP_API_KEY")
# 3) Download it right away (valid 1 h).
curl -o photo.jpg "$SIGNED"
Ils ne sont pas dans GET /v1/messages/inbound. Cette route ne liste que les messages reçus (direction: "in") et ceux envoyés depuis le téléphone en Coexistence ("echo"). Pour vos envois par l'API, l'API ne garde que la trace d'envoi : GET /v1/messages/{id} (statut) et GET /v1/logs?category=sends (modèle, destinataire masqué, statut, crédits). Pas le texte envoyé. Tenez donc votre propre journal d'envoi : enregistrez, au moment de l'appel, le texte ou le template, les variables, et l'id renvoyé.
Attention
Piège : sans copie de votre côté, tout ce qui dépasse la rétention de votre plan est perdu pour de bon. La purge est définitive.
Attention
Piège : l'adresse signée du média expire au bout d'1 heure. Une adresse conservée dans votre base ne marchera plus demain : téléchargez le fichier, ou rappelez GET /v1/media/{id} pour en obtenir une nouvelle tant que le message existe.
Attention
Piège : pour ne pas traiter deux fois le même message (rattrapage après un webhook déjà reçu), dédoublonnez sur wa_message_id.
Pour le webhook, voir Vérifier la signature d'un webhook. Pour recevoir une photo dans une conversation, voir Recevoir une photo ou un document.
Preuve — vérifié dans le code de l'API : public_api.py, api_inbound.py, storage_plans.py, api_usage.py, webhooks.py.