Documentation
Inbound webhooks
Antwoorden van je gekoppelde WhatsApp-nummer realtime ontvangen. Drop-in compatibel met TextMeBot — bestaande integraties die type / from / from_name / to / file / message verwerken werken zonder aanpassingen.
1. Endpoint configureren
Open /admin/webhook in het customer portal:
- Plak je publieke HTTPS URL.
- Klik Opslaan — er wordt automatisch een 64-char HMAC-secret gegenereerd. Kopieer hem direct, hij wordt nadien gemaskeerd.
- Klik Test fire om een sample-payload naar je endpoint te sturen.
2. Payload (TextMeBot-compatibel)
Bij elk inkomend bericht doet TextMeFlow een POST met content-type application/json en deze body:
{
"type": "text",
"from": "32472932208",
"from_name": "Jan Janssens",
"to": "32499123456",
"file": "null",
"message": "Bedankt voor de bevestiging!",
"from_me": false,
"wa_message_id": "3EB0A1B2C3D4E5F6",
"message_id": null
}
| Veld | Type | Inhoud |
|---|---|---|
| type | string | text, image, audio, video, document of location |
| from | string | Afzender, alleen cijfers (geen +) |
| from_name | string | WhatsApp pushName, mogelijk leeg |
| to | string | Jouw gekoppelde nummer, alleen cijfers |
| file | string | Voor text-berichten letterlijk de string "null" (matcht TextMeBot). Voor image, video, audio en document de URL van het bestand, door TextMeFlow gehost. Een foto zonder bijschrift komt binnen met een lege message. |
| message | string | Tekst of bijschrift; voor document de filename, voor location de string "lat,lng" |
| from_me | boolean | true voor een bericht dat jíj verstuurde — via de API óf met de hand in WhatsApp. Zo zie je het volledige gesprek in je eigen systeem. Bij true is from jouw nummer en to dat van de klant. |
| wa_message_id | string | Het WhatsApp-id van het bericht, in beide richtingen. Uniek per bericht; bruikbaar als ontdubbelsleutel. |
| message_id | string | null | Enkel bij from_me: true én verstuurd via de API: het message_id dat je bij het versturen terugkreeg. Daaraan herken je je eigen verzending en sla je ze niet dubbel op. null voor handmatig getikte berichten en voor alles wat binnenkomt. |
3. Headers
| Header | Inhoud |
|---|---|
| Content-Type | application/json |
| User-Agent | TextMeFlow-Webhook/1.0 |
| X-TextMeFlow-Event-Id | UUID per bericht — gebruik dit als idempotency-key, retries hergebruiken hetzelfde id. |
| X-TextMeFlow-Signature | sha256=<hex> — HMAC-SHA256 van de raw body met je webhook-secret. |
| X-TextMeFlow-Delivery | UUID per delivery-poging, handig voor log-grep. |
4. Signature verifiëren
Bereken HMAC-SHA256 over de raw request body (niet over een geparseerd object — JSON-encoder verschillen breken anders je hash) en vergelijk constant-time met de sha256=… waarde uit de header.
PHP
// $secret = 'tmf_…' uit /admin/webhook $raw = file_get_contents('php://input'); $expected = 'sha256=' . hash_hmac('sha256', $raw, $secret); $received = $_SERVER['HTTP_X_TEXTMEFLOW_SIGNATURE'] ?? ''; if (! hash_equals($expected, $received)) { http_response_code(401); exit; } $body = json_decode($raw, true);
Node.js (Express)
import crypto from 'node:crypto'; import express from 'express'; const app = express(); // Capture raw body for HMAC; can't run after express.json() ate it. app.use(express.raw({ type: 'application/json' })); app.post('/textmeflow-inbound', (req, res) => { const raw = req.body; // Buffer const expected = 'sha256=' + crypto .createHmac('sha256', process.env.TMF_WEBHOOK_SECRET) .update(raw) .digest('hex'); const received = req.get('X-TextMeFlow-Signature') ?? ''; if ( expected.length !== received.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)) ) { return res.sendStatus(401); } const body = JSON.parse(raw.toString('utf8')); // …handle body… res.sendStatus(200); });
Python (Flask)
import hmac, hashlib, os from flask import Flask, request, abort app = Flask(__name__) SECRET = os.environ['TMF_WEBHOOK_SECRET'].encode() @app.post('/textmeflow-inbound') def inbound(): raw = request.get_data() expected = 'sha256=' + hmac.new(SECRET, raw, hashlib.sha256).hexdigest() received = request.headers.get('X-TextMeFlow-Signature', '') if not hmac.compare_digest(expected, received): abort(401) body = request.get_json() # …handle body… return '', 200
5. Retry-policy
Een delivery is gelukt bij elke 2xx response binnen 10s. Andere uitkomsten:
| Response | Gedrag |
|---|---|
| 2xx | ✅ delivered, geen retry. |
| 408 / 429 / 5xx | 🔁 retry — max 5 keer met backoff 1m, 5m, 30m, 2h, 6h. |
| andere 4xx | ❌ failed, geen retry. Een 401 betekent meestal dat je secret niet meer matcht — controleer en fix bij jouw kant. |
| timeout / netwerk | 🔁 retry zoals 5xx. |
Na 6 totale pogingen (eerste + 5 retries) wordt de delivery als failed gemarkeerd. Status van elke poging zie je in /admin/webhook.
6. Migreren vanaf TextMeBot
Geen code-wijzigingen nodig — payload-shape is identiek. Eén actie:
- Plak je bestaande TextMeBot-endpoint URL in /admin/webhook.
- Voeg een signature-check toe (TextMeBot heeft die niet — verplichte upgrade voor productie). Zie de snippets hierboven.
Klaar om te ontvangen?
Configureer je endpoint in /admin/webhook en stuur een test-payload met één klik.