Aller au contenu principal

Événements Webhooks

Cette page détaille le format de chaque type d'événement webhook envoyé par ZimSend.

Structure générale

Tous les webhooks suivent cette structure de base :

{
"event": "nom_de_levenement",
"event_id": "evt_unique_identifier",
"timestamp": "2025-01-15T10:30:08Z",
"data": {
// Données spécifiques à l'événement
}
}
ChampTypeDescription
eventstringType d'événement
event_idstringIdentifiant unique de l'événement (pour éviter les doublons)
timestampstringDate/heure de l'événement (ISO 8601)
dataobjectDonnées spécifiques à l'événement

Événements SMS

sms.sent

Déclenché lorsqu'un SMS est envoyé au réseau mobile.

{
"event": "sms.sent",
"event_id": "evt_abc123def456",
"timestamp": "2025-01-15T10:30:05Z",
"data": {
"message_id": "msg_1234567890abcdef",
"status": "sent",
"to": "+33612345678",
"message": "Bonjour, votre commande a été expédiée",
"device_id": "device_xyz123",
"sent_at": "2025-01-15T10:30:05Z",
"metadata": {
"order_id": "order_12345",
"user_id": "user_789"
}
}
}

Champs du data :

ChampTypeDescription
message_idstringIdentifiant unique du SMS
statusstringStatut actuel (sent)
tostringNuméro du destinataire
messagestringContenu du SMS
device_idstringDevice utilisé pour l'envoi
sent_atstringDate/heure d'envoi
metadataobjectMétadonnées personnalisées (si fournies)

sms.delivered

Déclenché lorsqu'un SMS est délivré au destinataire.

{
"event": "sms.delivered",
"event_id": "evt_def456ghi789",
"timestamp": "2025-01-15T10:30:08Z",
"data": {
"message_id": "msg_1234567890abcdef",
"status": "delivered",
"to": "+33612345678",
"message": "Bonjour, votre commande a été expédiée",
"device_id": "device_xyz123",
"sent_at": "2025-01-15T10:30:05Z",
"delivered_at": "2025-01-15T10:30:08Z",
"metadata": {
"order_id": "order_12345",
"user_id": "user_789"
}
}
}

Champs du data :

ChampTypeDescription
message_idstringIdentifiant unique du SMS
statusstringStatut actuel (delivered)
tostringNuméro du destinataire
messagestringContenu du SMS
device_idstringDevice utilisé pour l'envoi
sent_atstringDate/heure d'envoi
delivered_atstringDate/heure de délivrance
metadataobjectMétadonnées personnalisées (si fournies)

sms.failed

Déclenché lorsqu'un SMS n'a pas pu être envoyé.

{
"event": "sms.failed",
"event_id": "evt_ghi789jkl012",
"timestamp": "2025-01-15T10:30:10Z",
"data": {
"message_id": "msg_1234567890abcdef",
"status": "failed",
"to": "+33612345678",
"message": "Bonjour, votre commande a été expédiée",
"device_id": "device_xyz123",
"error": {
"code": "NETWORK_ERROR",
"message": "Impossible de joindre le réseau mobile"
},
"failed_at": "2025-01-15T10:30:10Z",
"metadata": {
"order_id": "order_12345",
"user_id": "user_789"
}
}
}

Champs du data :

ChampTypeDescription
message_idstringIdentifiant unique du SMS
statusstringStatut actuel (failed)
tostringNuméro du destinataire
messagestringContenu du SMS
device_idstringDevice utilisé pour l'envoi
errorobjectDétails de l'erreur
error.codestringCode d'erreur
error.messagestringMessage d'erreur
failed_atstringDate/heure de l'échec
metadataobjectMétadonnées personnalisées (si fournies)

Codes d'erreur possibles :

CodeDescription
NETWORK_ERRORErreur réseau mobile
INVALID_NUMBERNuméro de téléphone invalide
NO_CREDITCrédit téléphone insuffisant
BLOCKED_NUMBERNuméro bloqué ou blacklisté
DEVICE_ERRORErreur sur le device Android
TIMEOUTTimeout lors de l'envoi

Événements Device

device.connected

Déclenché lorsqu'un device Android se connecte.

{
"event": "device.connected",
"event_id": "evt_jkl012mno345",
"timestamp": "2025-01-15T09:00:00Z",
"data": {
"device_id": "device_xyz123",
"device_name": "Samsung Galaxy S21",
"phone_number": "+33612345678",
"android_version": "12",
"app_version": "1.2.3",
"connected_at": "2025-01-15T09:00:00Z",
"ip_address": "192.168.1.100"
}
}

Champs du data :

ChampTypeDescription
device_idstringIdentifiant unique du device
device_namestringNom du device (modèle)
phone_numberstringNuméro de téléphone de la SIM
android_versionstringVersion Android
app_versionstringVersion de l'app ZimSend
connected_atstringDate/heure de connexion
ip_addressstringAdresse IP du device

device.disconnected

Déclenché lorsqu'un device Android se déconnecte.

{
"event": "device.disconnected",
"event_id": "evt_mno345pqr678",
"timestamp": "2025-01-15T18:30:00Z",
"data": {
"device_id": "device_xyz123",
"device_name": "Samsung Galaxy S21",
"phone_number": "+33612345678",
"disconnected_at": "2025-01-15T18:30:00Z",
"reason": "CONNECTION_LOST",
"last_seen": "2025-01-15T18:29:55Z"
}
}

Champs du data :

ChampTypeDescription
device_idstringIdentifiant unique du device
device_namestringNom du device (modèle)
phone_numberstringNuméro de téléphone de la SIM
disconnected_atstringDate/heure de déconnexion
reasonstringRaison de la déconnexion
last_seenstringDernière activité du device

Raisons de déconnexion possibles :

RaisonDescription
CONNECTION_LOSTPerte de connexion Internet
USER_LOGOUTDéconnexion manuelle par l'utilisateur
APP_CLOSEDApplication fermée
BATTERY_SAVERMode économie d'énergie activé
UNKNOWNRaison inconnue

Exemples d'implémentation

Node.js - Traiter tous les événements

const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = process.env.ZIMSEND_WEBHOOK_SECRET;

app.post('/webhooks/zimsend', express.raw({type: 'application/json'}), (req, res) => {
// Vérifier la signature
const signature = req.headers['x-webhook-signature'];
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');

if (signature !== expectedSignature) {
return res.status(401).send('Unauthorized');
}

// Parser l'événement
const event = JSON.parse(req.body);

// Répondre immédiatement
res.status(200).send('OK');

// Traiter selon le type d'événement
switch(event.event) {
case 'sms.sent':
handleSMSSent(event.data);
break;

case 'sms.delivered':
handleSMSDelivered(event.data);
break;

case 'sms.failed':
handleSMSFailed(event.data);
break;

case 'device.connected':
handleDeviceConnected(event.data);
break;

case 'device.disconnected':
handleDeviceDisconnected(event.data);
break;

default:
console.log('Événement inconnu:', event.event);
}
});

function handleSMSSent(data) {
console.log(`SMS ${data.message_id} envoyé à ${data.to}`);

// Mettre à jour la base de données
db.updateSMS(data.message_id, {
status: 'sent',
sent_at: data.sent_at
});
}

function handleSMSDelivered(data) {
console.log(`SMS ${data.message_id} délivré à ${data.to}`);

// Mettre à jour la base de données
db.updateSMS(data.message_id, {
status: 'delivered',
delivered_at: data.delivered_at
});

// Notifier l'utilisateur
if (data.metadata?.user_id) {
notifyUser(data.metadata.user_id, 'Votre message a été délivré');
}
}

function handleSMSFailed(data) {
console.error(`SMS ${data.message_id} échoué:`, data.error);

// Mettre à jour la base de données
db.updateSMS(data.message_id, {
status: 'failed',
error: data.error,
failed_at: data.failed_at
});

// Alerter l'équipe
alertTeam(`SMS échoué: ${data.error.message}`);

// Réessayer avec un autre device si possible
if (data.error.code === 'DEVICE_ERROR') {
retryWithDifferentDevice(data);
}
}

function handleDeviceConnected(data) {
console.log(`Device ${data.device_name} connecté`);

// Mettre à jour le statut du device
db.updateDevice(data.device_id, {
status: 'online',
connected_at: data.connected_at,
ip_address: data.ip_address
});

// Notifier l'équipe
notifyTeam(`Device ${data.device_name} est maintenant en ligne`);
}

function handleDeviceDisconnected(data) {
console.warn(`Device ${data.device_name} déconnecté: ${data.reason}`);

// Mettre à jour le statut du device
db.updateDevice(data.device_id, {
status: 'offline',
disconnected_at: data.disconnected_at,
disconnect_reason: data.reason
});

// Alerter si déconnexion inattendue
if (data.reason === 'CONNECTION_LOST') {
alertTeam(`Device ${data.device_name} a perdu la connexion`);
}
}

app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});

Python - Traiter avec une queue asynchrone

from flask import Flask, request, abort
import hmac
import hashlib
import json
from celery import Celery

app = Flask(__name__)
celery = Celery('tasks', broker='redis://localhost:6379')

WEBHOOK_SECRET = os.getenv('ZIMSEND_WEBHOOK_SECRET')

@app.route('/webhooks/zimsend', methods=['POST'])
def webhook():
# Vérifier la signature
signature = request.headers.get('X-Webhook-Signature')
body = request.get_data()

expected_signature = hmac.new(
WEBHOOK_SECRET.encode(),
body,
hashlib.sha256
).hexdigest()

if signature != expected_signature:
abort(401)

# Parser l'événement
event = request.json

# Répondre immédiatement
response = ('OK', 200)

# Traiter de manière asynchrone avec Celery
process_webhook.delay(event)

return response

@celery.task
def process_webhook(event):
"""Traiter l'événement de manière asynchrone"""
event_type = event['event']
data = event['data']

handlers = {
'sms.sent': handle_sms_sent,
'sms.delivered': handle_sms_delivered,
'sms.failed': handle_sms_failed,
'device.connected': handle_device_connected,
'device.disconnected': handle_device_disconnected
}

handler = handlers.get(event_type)
if handler:
handler(data)
else:
print(f"Événement inconnu: {event_type}")

def handle_sms_sent(data):
print(f"SMS {data['message_id']} envoyé à {data['to']}")
# Mettre à jour la base de données
db.update_sms(data['message_id'], {
'status': 'sent',
'sent_at': data['sent_at']
})

def handle_sms_delivered(data):
print(f"SMS {data['message_id']} délivré à {data['to']}")
# Mettre à jour la base de données
db.update_sms(data['message_id'], {
'status': 'delivered',
'delivered_at': data['delivered_at']
})

def handle_sms_failed(data):
print(f"SMS {data['message_id']} échoué: {data['error']}")
# Alerter l'équipe
alert_team(f"SMS échoué: {data['error']['message']}")

def handle_device_connected(data):
print(f"Device {data['device_name']} connecté")
db.update_device(data['device_id'], {'status': 'online'})

def handle_device_disconnected(data):
print(f"Device {data['device_name']} déconnecté: {data['reason']}")
db.update_device(data['device_id'], {'status': 'offline'})

if __name__ == '__main__':
app.run()

PHP - Traiter avec une queue Redis

<?php

require 'vendor/autoload.php';

use Predis\Client as RedisClient;

$webhookSecret = getenv('ZIMSEND_WEBHOOK_SECRET');
$redis = new RedisClient();

// Vérifier la signature
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

$expectedSignature = hash_hmac('sha256', $body, $webhookSecret);

if ($signature !== $expectedSignature) {
http_response_code(401);
exit('Unauthorized');
}

// Parser l'événement
$event = json_decode($body, true);

// Répondre immédiatement
http_response_code(200);
echo 'OK';

// Ajouter à la queue Redis pour traitement asynchrone
$redis->rpush('zimsend_webhooks', json_encode($event));

// Le worker Redis traitera les événements
// worker.php:
/*
while (true) {
$eventJson = $redis->blpop('zimsend_webhooks', 0)[1];
$event = json_decode($eventJson, true);

switch($event['event']) {
case 'sms.sent':
handleSMSSent($event['data']);
break;
case 'sms.delivered':
handleSMSDelivered($event['data']);
break;
case 'sms.failed':
handleSMSFailed($event['data']);
break;
case 'device.connected':
handleDeviceConnected($event['data']);
break;
case 'device.disconnected':
handleDeviceDisconnected($event['data']);
break;
}
}
*/
?>

Retry Logic

Si votre endpoint webhook échoue, ZimSend réessaiera automatiquement :

  • 1ère tentative : Immédiate
  • 2ème tentative : Après 1 minute
  • 3ème tentative : Après 5 minutes

Après 3 échecs, l'événement est abandonné.

Important : Implémentez l'idempotence en vérifiant event_id pour éviter de traiter deux fois le même événement.

Prochaines étapes