Démarrage rapide #
Trois étapes, et vous envoyez votre premier message.
-
1
Créer un compte
Inscrivez-vous sur Callisto. C'est gratuit.
-
2
Obtenir une clé API
Créez-la depuis votre tableau de bord.
-
3
Envoyer un SMS ou un message WhatsApp
Faites votre premier appel à l'API.
Authentification #
L'API utilise l'authentification HTTP Basic. Chaque requête doit porter un en-tête Authorization contenant vos identifiants, encodés en base64.
Vos identifiants
Chaque clé API fournit deux valeurs : access_key, un identifiant public, et access_secret, que vous devez garder secret. Vous les trouverez dans le tableau de bord, à l'adresse /api-keys.
Format de l'en-tête
Joignez les deux valeurs avec un deux-points, puis encodez le résultat en base64 :
Authorization: Basic <base64(access_key:access_secret)>
Exemple avec cURL
Avec cURL, l'option -u fait l'encodage pour vous :
curl -u 'ak_xxx:sk_xxx' https://api.callistosignal.com/v1/sms/balance
URL de base #
https://api.callistosignal.com/v1
Les requêtes POST envoient Content-Type: application/json, sans charset. Sinon, l'API répond 400.
Envoyer un SMS #
POST /v1/sms/sendParamètres
| Paramètre | Type | Requis | Description |
|---|---|---|---|
to |
string | array | Oui | Numéro du destinataire, ou liste de numéros, au format international |
message |
string | Oui | Texte du message (1530 caractères au plus) |
sender |
string | Non | Nom d'expéditeur (Sender ID, 11 caractères max), approuvé sur votre compte. Facultatif : voir « Nom d'expéditeur par défaut » ci-dessous. |
scheduled_at |
datetime | Non | Date et heure d'envoi, au format YYYY-MM-DD HH:MM:SS. |
skip_duplicated |
boolean | Non | Si true, un message identique (même numéro, même texte) déjà envoyé dans les dernières 24 h n'est ni renvoyé ni facturé : il apparaît dans skipped. |
Nom d'expéditeur par défaut
Sans sender, le SMS part sous le nom d'expéditeur par défaut de votre organisation, celui que vous marquez « Par défaut » dans vos expéditeurs. Si votre organisation n'en a pas, il part sous le nom d'expéditeur de la plateforme pour le réseau du destinataire. Un sender indiqué n'est jamais remplacé : s'il n'existe pas ou n'est pas approuvé, l'appel est refusé (400). L'appel est aussi refusé quand aucun nom n'est disponible.
Crédits insuffisants
Si votre solde ne couvre pas tous les destinataires, l'appel renvoie 402 avec le code INSUFFICIENT_CREDITS. Rien n'est débité et aucun message n'est créé. Un numéro invalide, un pays non pris en charge ou une organisation sans pays (à renseigner dans Paramètres > Organisation) renvoie 400 ; rien n'est débité.
{
"message": "Insufficient credits. Required: 25, Available: 10",
"code": "INSUFFICIENT_CREDITS",
"trace": []
}
Exemple de requête
// Envoyer un SMS
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/sms/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + credentials
},
body: JSON.stringify({
to: '+22507XXXXXXXX',
message: 'Bonjour',
sender: 'MyApp'
})
});
if (!res.ok) {
// 400, 401, 429… : le corps porte le motif du refus
const { message } = await res.json();
throw new Error(`SMS refusé (${res.status}) : ${message}`);
}
const data = await res.json();
// Envoyer un SMS via cURL
$ch = curl_init('https://api.callistosignal.com/v1/sms/send');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'to' => '+22507XXXXXXXX',
'message' => 'Bonjour',
'sender' => 'MyApp',
]),
]);
$response = curl_exec($ch);
curl_close($ch);
# Envoyer un SMS
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
-X POST https://api.callistosignal.com/v1/sms/send \
-H 'Content-Type: application/json' \
-d '{
"to": "+22507XXXXXXXX",
"message": "Bonjour",
"sender": "MyApp"
}'
Réponse
{
"total_amount": 25,
"available_credit": 975,
"status": "sent",
"recipient_count": 1,
"scheduled": false,
"messages": [
{ "id": "550e8400-e29b-41d4-a716-446655440000", "recipient": "22507XXXXXXXX" }
],
"skipped": []
}
Envoi en masse #
POST /v1/sms/sendEnvoyez le même message à plusieurs numéros en une seule requête.
Exemple de requête
// Envoyer un SMS en masse
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/sms/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + credentials
},
body: JSON.stringify({
to: ['+22507XXXXXXXX', '+22508XXXXXXXX'],
message: 'Bonjour à tous',
sender: 'MyApp'
})
});
// Envoyer un SMS en masse
$ch = curl_init('https://api.callistosignal.com/v1/sms/send');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'to' => ['+22507XXXXXXXX', '+22508XXXXXXXX'],
'message' => 'Bonjour à tous',
'sender' => 'MyApp',
]),
]);
$response = curl_exec($ch);
# Envoyer un SMS en masse
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
-X POST https://api.callistosignal.com/v1/sms/send \
-H 'Content-Type: application/json' \
-d '{
"to": ["+22507XXXXXXXX", "+22508XXXXXXXX"],
"message": "Bonjour à tous",
"sender": "MyApp"
}'
Lister les SMS #
GET /v1/sms/messagesListez les SMS envoyés par votre organisation, page par page.
Paramètres de requête
| Paramètre | Type | Par défaut | Description |
|---|---|---|---|
page |
integer | 1 | Numéro de page. |
per_page |
integer | 15 | Éléments par page, 100 au plus. |
started_at |
datetime | aujourd'hui 00:00:00 | Début de la période (YYYY-MM-DD HH:MM:SS). |
ended_at |
datetime | aujourd'hui 23:59:59 | Fin de la période (YYYY-MM-DD HH:MM:SS). |
Exemple de requête
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/sms/messages?page=1&per_page=50', {
headers: { 'Authorization': 'Basic ' + credentials }
});
$ch = curl_init('https://api.callistosignal.com/v1/sms/messages?page=1&per_page=50');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
'https://api.callistosignal.com/v1/sms/messages?page=1&per_page=50'
La réponse a la même forme paginée que la liste des OTP : items, total, per_page, current_page, next, previous, total_pages.
OTP : envoyer un code #
POST /v1/otp/send/otp/send
Envoyez un code de vérification à usage unique (OTP) à un numéro. Callisto génère le code et l'insère dans votre message à la place de {code}.
Paramètres
| Paramètre | Type | Requis | Description |
|---|---|---|---|
to |
string | Oui | Numéro du destinataire, au format international |
message |
string | Oui | Votre message. Il doit contenir {code} (5-500 caractères). |
sender |
string | Non | Nom d'expéditeur (Sender ID), approuvé sur votre compte. Facultatif : sans lui, le nom par défaut de votre organisation, puis celui de la plateforme (voir l'envoi de SMS). Ignoré si le code part sur WhatsApp. |
expired_in |
integer | Oui | Durée de validité du code, en secondes (60 - 86400) |
digit_size |
integer | Non | Longueur du code (4-16, défaut : 6) |
type |
string | Non | Type de code : digit, alpha ou alphanumeric (défaut : digit). Les codes en lettres sont en majuscules, et la vérification respecte la casse. |
provider |
string | Non | Canal d'envoi du code : sms (défaut) ou whatsapp |
instanceCode |
string | Conditionnel | Code de l'instance WhatsApp qui envoie le message. Requis si provider = whatsapp. |
Exemple de requête
// Envoyer un code OTP
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/otp/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + credentials
},
body: JSON.stringify({
to: '+22507XXXXXXXX',
message: 'Votre code de vérification est {code}',
sender: 'MyApp',
digit_size: 6,
expired_in: 300,
type: 'digit',
provider: 'sms'
})
});
if (!res.ok) {
// 400, 401, 429… : le corps porte le motif du refus
const { message } = await res.json();
throw new Error(`OTP refusé (${res.status}) : ${message}`);
}
const { id, recipient, expires_at } = await res.json();
// Envoyer un code OTP
$ch = curl_init('https://api.callistosignal.com/v1/otp/send');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'to' => '+22507XXXXXXXX',
'message' => 'Votre code de vérification est {code}',
'sender' => 'MyApp',
'digit_size' => 6,
'expired_in' => 300,
'type' => 'digit',
'provider' => 'sms',
]),
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
-X POST https://api.callistosignal.com/v1/otp/send \
-H 'Content-Type: application/json' \
-d '{
"to": "+22507XXXXXXXX",
"message": "Votre code de vérification est {code}",
"sender": "MyApp",
"digit_size": 6,
"expired_in": 300,
"type": "digit",
"provider": "sms"
}'
Réponse
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"provider": "sms",
"recipient": "+22507XXXXXXXX",
"expires_at": "2026-04-07T10:35:00Z",
"expires_in": 300
}
Envoyer le code par WhatsApp
Pour envoyer le code depuis une de vos instances WhatsApp connectées, ajoutez provider: "whatsapp" et instanceCode. Le champ sender est alors ignoré.
{
"to": "+22507XXXXXXXX",
"message": "Votre code Callisto est {code}",
"expired_in": 300,
"digit_size": 6,
"type": "digit",
"provider": "whatsapp",
"instanceCode": "{instanceCode}"
}
La réponse est la même, avec "provider": "whatsapp". L'API renvoie 400 si instanceCode manque ou si l'instance n'est pas connectée.
Un code envoyé par SMS ("provider": "sms") coûte 1 crédit. S'il vous manque, l'appel renvoie 402 : aucun code n'est créé et rien n'est débité.
{
"message": "Insufficient credits. Required: 1, Available: 0"
}
Note : Gardez l'id renvoyé dans la réponse. Il vous servira à vérifier le code.
OTP : vérifier un code #
POST /v1/otp/verify/otp/verify
Vérifiez le code saisi par votre utilisateur.
Paramètres
| Paramètre | Type | Requis | Description |
|---|---|---|---|
otp_id |
string | Oui | L'ID de l'OTP, renvoyé à l'envoi du code (UUID) |
code |
string | Oui | Le code saisi par l'utilisateur (4-16 caractères) |
Exemple de requête
// Vérifier un code OTP
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/otp/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + credentials
},
body: JSON.stringify({
otp_id: '550e8400-e29b-41d4-a716-446655440000',
code: '123456'
})
});
// Vérifier un code OTP
$ch = curl_init('https://api.callistosignal.com/v1/otp/verify');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'otp_id' => '550e8400-e29b-41d4-a716-446655440000',
'code' => '123456',
]),
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
-X POST https://api.callistosignal.com/v1/otp/verify \
-H 'Content-Type: application/json' \
-d '{
"otp_id": "550e8400-e29b-41d4-a716-446655440000",
"code": "123456"
}'
Réponse (succès)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "verified",
"verified": true,
"verified_at": "2026-04-07T10:32:15Z"
}
Réponse (échec)
{
"message": "Invalid OTP code. 2 attempts remaining"
}
Codes d'erreur OTP
| Code | Description |
|---|---|
400 |
Code faux. Le message indique le nombre de tentatives restantes. |
400 |
Code expiré |
400 |
Trois codes faux déjà saisis : l'OTP est bloqué |
404 |
OTP introuvable : l'otp_id n'existe pas, appartient à une autre organisation ou n'est pas un UUID. Le message est le même dans les trois cas. |
OTP : statut #
GET /v1/otps/{id}/otps/{{otp_id}}
Consultez le statut actuel d'un OTP.
Exemple de requête
// Consulter le statut d'un code OTP
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/otps/550e8400-e29b-41d4-a716-446655440000', {
headers: { 'Authorization': 'Basic ' + credentials }
});
// Consulter le statut d'un code OTP
$ch = curl_init('https://api.callistosignal.com/v1/otps/550e8400-e29b-41d4-a716-446655440000');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
https://api.callistosignal.com/v1/otps/550e8400-e29b-41d4-a716-446655440000
Réponse
{
"otp_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"recipient": "+22507XXXXXXXX",
"expires_at": "2026-04-07T10:35:00Z",
"verified_at": null,
"attempts": 0,
"created_at": "2026-04-07T10:30:00Z"
}
Statuts possibles
| Statut | Description |
|---|---|
| pending | Pas encore vérifié |
| verified | Le code a été vérifié |
| expired | Le code a expiré |
| failed | Trois codes faux saisis, l'OTP est bloqué |
Un OTP introuvable renvoie 404.
OTP : liste #
GET /v1/otps/otps
Listez vos OTP, page par page.
Paramètres de requête
| Paramètre | Type | Par défaut | Description |
|---|---|---|---|
page |
integer | 1 | Numéro de page |
limit |
integer | 20 | Éléments par page, 100 au plus |
started_at |
date | -30 jours | Date de début (YYYY-MM-DD) |
ended_at |
date | aujourd'hui | Date de fin (YYYY-MM-DD) |
Exemple de requête
// Lister les codes OTP envoyés
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/otps?limit=50', {
headers: { 'Authorization': 'Basic ' + credentials }
});
// Lister les codes OTP envoyés
$ch = curl_init('https://api.callistosignal.com/v1/otps?limit=50');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
'https://api.callistosignal.com/v1/otps?limit=50'
Réponse
{
"items": [
{
"id": "3b5d654b-d603-44b6-8f9e-0f24b38eafd0",
"organization_id": "9d8e0eab-6c65-4794-ae22-637445a421bf",
"api_key_id": "c11e9a92-27eb-430f-949f-40dea00d521d",
"message_id": "ea01e0ba-02d2-471a-8e23-340b7d570b3e",
"recipient": "+22507XXXXXXXX",
"status": "expired",
"expires_at": "2026-04-07 19:05:12",
"verified_at": null,
"attempts": 0,
"created_at": "2026-04-07 18:06:52",
"updated_at": "2026-04-07 18:06:52"
},
{
"id": "d5f29dfc-03b5-41da-b371-2d7998896c72",
"organization_id": "9d8e0eab-6c65-4794-ae22-637445a421bf",
"api_key_id": "c11e9a92-27eb-430f-949f-40dea00d521d",
"message_id": "ac118655-fe08-4eba-afb1-e4a7a5399a95",
"recipient": "+22508XXXXXXXX",
"status": "verified",
"expires_at": "2026-04-07 18:55:37",
"verified_at": "2026-04-07 18:52:15",
"attempts": 1,
"created_at": "2026-04-07 17:57:17",
"updated_at": "2026-04-07 18:52:15"
}
],
"total": 25,
"per_page": 20,
"current_page": 1,
"next": 2,
"previous": 0,
"total_pages": 2
}
Connecter une instance #
Avant d'envoyer quoi que ce soit sur WhatsApp, vous associez un numéro de téléphone à Callisto. Cela se fait une seule fois, en scannant un QR code dans le tableau de bord.
Étapes
- Connectez-vous à votre tableau de bord.
- Allez dans WhatsApp → Instances → Nouvelle instance.
- Donnez un nom à l'instance, par exemple « Production » ou « Support ».
- Sur le téléphone qui hébergera l'instance, ouvrez WhatsApp ou WhatsApp Business et scannez le QR code affiché à l'écran.
- Dès que l'instance affiche
status: connected, vous pouvez envoyer des messages par l'API.
Instances #
Listez vos instances WhatsApp associées et voyez le statut de chacune.
Lister les instances
GET /v1/whatsapp/instances// Lister les instances WhatsApp
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/whatsapp/instances', {
headers: { 'Authorization': 'Basic ' + credentials }
});
if (!res.ok) {
// 401, 429… : le corps porte le motif du refus
const { message } = await res.json();
throw new Error(`Liste refusée (${res.status}) : ${message}`);
}
const instances = await res.json();
// Lister les instances WhatsApp
$ch = curl_init('https://api.callistosignal.com/v1/whatsapp/instances');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
https://api.callistosignal.com/v1/whatsapp/instances
Réponse (200)
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"code": "6650a1b2c3d4e",
"name": "Production",
"phone_number": "+22507XXXXXXXX",
"phone_name": "Callisto Demo",
"status": "connected",
"billing_status": "active",
"trial_days_remaining": 0,
"monthly_fee": 5000,
"created_at": "2026-04-12T10:34:00Z"
}
],
"total": 1,
"per_page": 15,
"current_page": 1,
"next": 1,
"previous": 1,
"total_pages": 1
}
La liste est paginée, 15 instances par page. Passez ?page=2 pour les pages suivantes.
Détails d'une instance
GET /v1/whatsapp/{instanceCode}
Paramètre de chemin : instanceCode : le champ code de l'instance (13 caractères hexadécimaux, par exemple 6650a1b2c3d4e). Ce n'est pas son id.
// Consulter une instance WhatsApp
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/whatsapp/{instanceCode}', {
headers: { 'Authorization': 'Basic ' + credentials }
});
if (!res.ok) {
// 404 si l'instance n'existe pas, 401, 429… : le corps porte le motif
const { message } = await res.json();
throw new Error(`Instance introuvable ou refusée (${res.status}) : ${message}`);
}
const instance = await res.json();
// Consulter une instance WhatsApp
$ch = curl_init('https://api.callistosignal.com/v1/whatsapp/{instanceCode}');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
https://api.callistosignal.com/v1/whatsapp/{instanceCode}
Réponse (200)
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"code": "6650a1b2c3d4e",
"name": "Production",
"phone_number": "+22507XXXXXXXX",
"phone_name": "Callisto Demo",
"status": "connected",
"billing_status": "active",
"trial_days_remaining": 0,
"monthly_fee": 5000,
"created_at": "2026-04-12T10:34:00Z"
}
Envoyer un message #
Une fois votre instance associée et connected, vous pouvez envoyer par l'API des messages texte, média ou de localisation.
Message texte
POST /v1/whatsapp/{instanceCode}/send/textParamètres du corps
| Paramètre | Type | Requis | Description |
|---|---|---|---|
| to | string | Oui | Numéro du destinataire au format E.164 (ex. +22507XXXXXXXX). |
| message | string | Oui | Le texte à envoyer (4096 caractères au plus). |
| scheduled_at | datetime | Non | Envoyer plus tard, à cette date et heure (YYYY-MM-DD HH:MM:SS). |
| skip_duplicated | boolean | Non | Si true et qu'un message identique a déjà été envoyé à ce numéro dans les dernières 24 h, rien n'est envoyé : la réponse décrit ce message, avec skipped à true. |
// Envoyer un message texte WhatsApp
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/whatsapp/{instanceCode}/send/text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + credentials
},
body: JSON.stringify({
to: '+22507XXXXXXXX',
message: 'Bonjour Aïcha'
})
});
// Envoyer un message texte WhatsApp
$ch = curl_init('https://api.callistosignal.com/v1/whatsapp/{instanceCode}/send/text');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'to' => '+22507XXXXXXXX',
'message' => 'Bonjour Aïcha',
]),
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
-X POST https://api.callistosignal.com/v1/whatsapp/{instanceCode}/send/text \
-H 'Content-Type: application/json' \
-d '{"to":"+22507XXXXXXXX","message":"Bonjour Aïcha"}'
Réponse (200)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"instance_id": "550e8400-e29b-41d4-a716-446655440001",
"recipient": "+22507XXXXXXXX",
"message_type": "text",
"status": "queued",
"scheduled": false,
"skipped": false
}
status vaut queued, ou pending si l'envoi est programmé avec scheduled_at.
Message média
POST /v1/whatsapp/{instanceCode}/send/mediaParamètres du corps
| Paramètre | Type | Requis | Description |
|---|---|---|---|
| to | string | Oui | Numéro du destinataire (E.164). |
| type | string | Oui | image, video, document ou audio. |
| media_url | string | Oui | URL publique du fichier (http:// ou https://). |
| caption | string | Non | Légende affichée avec le média (max 1024 caractères). |
| filename | string | Non | Nom du fichier, utile pour document (max 255 caractères). |
| scheduled_at | datetime | Non | Envoyer plus tard, à cette date et heure (YYYY-MM-DD HH:MM:SS). |
| skip_duplicated | boolean | Non | Si true et qu'un message identique a déjà été envoyé à ce numéro dans les dernières 24 h, rien n'est envoyé : la réponse décrit ce message, avec skipped à true. |
// Envoyer un média WhatsApp
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/whatsapp/{instanceCode}/send/media', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + credentials
},
body: JSON.stringify({
to: '+22507XXXXXXXX',
type: 'image',
media_url: 'https://example.com/photo.jpg',
caption: 'Notre nouveau produit'
})
});
// Envoyer un média WhatsApp
$ch = curl_init('https://api.callistosignal.com/v1/whatsapp/{instanceCode}/send/media');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'to' => '+22507XXXXXXXX',
'type' => 'image',
'media_url' => 'https://example.com/photo.jpg',
'caption' => 'Notre nouveau produit',
]),
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
-X POST https://api.callistosignal.com/v1/whatsapp/{instanceCode}/send/media \
-H 'Content-Type: application/json' \
-d '{"to":"+22507XXXXXXXX","type":"image","media_url":"https://example.com/photo.jpg","caption":"Notre nouveau produit"}'
Réponse (200)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"instance_id": "550e8400-e29b-41d4-a716-446655440001",
"recipient": "+22507XXXXXXXX",
"message_type": "image",
"media_url": "https://example.com/photo.jpg",
"status": "queued",
"scheduled": false,
"skipped": false
}
Message de localisation
POST /v1/whatsapp/{instanceCode}/send/locationParamètres du corps
| Paramètre | Type | Requis | Description |
|---|---|---|---|
| to | string | Oui | Numéro du destinataire (E.164). |
| latitude | number | Oui | Latitude entre -90 et 90. |
| longitude | number | Oui | Longitude entre -180 et 180. |
| name | string | Non | Nom du lieu (≤ 255 caractères). |
| address | string | Non | Adresse postale (≤ 1024 caractères). |
| scheduled_at | datetime | Non | Envoyer plus tard, à cette date et heure (YYYY-MM-DD HH:MM:SS). |
| skip_duplicated | boolean | Non | Si true et qu'un message identique a déjà été envoyé à ce numéro dans les dernières 24 h, rien n'est envoyé : la réponse décrit ce message, avec skipped à true. |
// Envoyer une localisation WhatsApp
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/whatsapp/{instanceCode}/send/location', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + credentials
},
body: JSON.stringify({
to: '+22507XXXXXXXX',
latitude: 5.3599517,
longitude: -4.0082563,
name: 'Plateau, Abidjan',
address: 'Boulevard de la République, Abidjan'
})
});
// Envoyer une localisation WhatsApp
$ch = curl_init('https://api.callistosignal.com/v1/whatsapp/{instanceCode}/send/location');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'to' => '+22507XXXXXXXX',
'latitude' => 5.3599517,
'longitude' => -4.0082563,
'name' => 'Plateau, Abidjan',
'address' => 'Boulevard de la République, Abidjan',
]),
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
-X POST https://api.callistosignal.com/v1/whatsapp/{instanceCode}/send/location \
-H 'Content-Type: application/json' \
-d '{"to":"+22507XXXXXXXX","latitude":5.3599517,"longitude":-4.0082563,"name":"Plateau, Abidjan"}'
Réponse (200)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"instance_id": "550e8400-e29b-41d4-a716-446655440001",
"recipient": "+22507XXXXXXXX",
"message_type": "location",
"status": "queued",
"scheduled": false,
"skipped": false
}
Messages WhatsApp #
Listez les messages envoyés depuis une instance, ou consultez le statut d'un message précis.
Lister les messages d'une instance
GET /v1/whatsapp/{instanceCode}/messagesParamètres de requête
| Paramètre | Type | Par défaut | Description |
|---|---|---|---|
| page | int | 1 | Numéro de page (1 par défaut). |
| per_page | int | 15 | Éléments par page, 100 au plus. |
| started_at | datetime | — | Début / fin de la période (YYYY-MM-DD HH:MM:SS). Sans dates, seuls les messages du jour sont renvoyés. |
| ended_at | datetime | — | Début / fin de la période (YYYY-MM-DD HH:MM:SS). Sans dates, seuls les messages du jour sont renvoyés. |
// Lister les messages WhatsApp d'une instance
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/whatsapp/{instanceCode}/messages?page=1&per_page=50', {
headers: { 'Authorization': 'Basic ' + credentials }
});
// Lister les messages WhatsApp d'une instance
$ch = curl_init('https://api.callistosignal.com/v1/whatsapp/{instanceCode}/messages?page=1&per_page=50');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
'https://api.callistosignal.com/v1/whatsapp/{instanceCode}/messages?page=1&per_page=50'
Réponse (200)
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"instance_id": "550e8400-e29b-41d4-a716-446655440001",
"recipient": "+22507XXXXXXXX",
"message_type": "text",
"content": "Bonjour Aïcha",
"media_url": null,
"status": "delivered",
"scheduled_at": null,
"sent_at": "2026-05-14 10:34:00",
"delivered_at": "2026-05-14 10:34:02",
"read_at": null,
"error_code": null,
"error_message": null,
"created_at": "2026-05-14 10:33:59"
}
],
"total": 1,
"per_page": 50,
"current_page": 1,
"next": 1,
"previous": 1,
"total_pages": 1
}
Statut d'un message
GET /v1/whatsapp/messages/{messageId}
Paramètre de chemin : messageId, l'identifiant du message (ex. 550e8400-e29b-41d4-a716-446655440000).
// Consulter le statut d'un message WhatsApp
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/whatsapp/messages/550e8400-e29b-41d4-a716-446655440000', {
headers: { 'Authorization': 'Basic ' + credentials }
});
// Consulter le statut d'un message WhatsApp
$ch = curl_init('https://api.callistosignal.com/v1/whatsapp/messages/550e8400-e29b-41d4-a716-446655440000');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
https://api.callistosignal.com/v1/whatsapp/messages/550e8400-e29b-41d4-a716-446655440000
Réponse (200)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"instance_id": "550e8400-e29b-41d4-a716-446655440001",
"recipient": "+22507XXXXXXXX",
"message_type": "text",
"content": "Bonjour Aïcha",
"media_url": null,
"status": "delivered",
"scheduled_at": null,
"sent_at": "2026-05-14 10:34:00",
"delivered_at": "2026-05-14 10:34:02",
"read_at": null,
"error_code": null,
"error_message": null,
"created_at": "2026-05-14 10:33:59"
}
Valeurs possibles pour status : pending, queued, sending, sent, delivered, read, failed.
pending : envoi programmé avec scheduled_at.
Webhooks WhatsApp #
Callisto envoie un POST JSON à la webhook_url de l'instance pour chaque événement : message.sent, message.failed, message.status, message.received, instance.qr, instance.connected, instance.disconnected.
Le corps contient event, instance_id, timestamp, occurred_at, instance, data.
Vérifiez l'en-tête X-Callisto-Signature (sha256= suivi du HMAC-SHA256 du corps brut, calculé avec le secret de l'instance) et ignorez les doublons grâce à X-Callisto-Delivery, qui reste le même à chaque tentative. Sans secret sur l'instance, aucune signature n'est envoyée.
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, $instanceSecret);
$valid = hash_equals($expected, $_SERVER['HTTP_X_CALLISTO_SIGNATURE'] ?? '');
Seule une réponse 2xx compte comme reçue. Votre endpoint a 10 s pour répondre. Un envoi qui échoue (erreur réseau, réponse 5xx, 408, 425 ou 429) est retenté cinq fois, après 1 min, 5 min, 30 min, 2 h puis 6 h, soit environ 8 h 30 au total. Toute autre réponse (3xx, 4xx) arrête les tentatives : les redirections ne sont pas suivies.
Topics et intégrations #
Une intégration, c'est un fournisseur que vous connectez avec vos propres identifiants : Slack, Telegram, un service d'e-mail, Amazon SNS, Firebase, Pusher ou votre propre webhook. Un topic regroupe des intégrations sous un même nom, par exemple orders. Vous publiez un événement sur le topic, et Callisto le livre à chaque intégration rattachée qui gère ce type d'événement.
Mise en place
- Dans le tableau de bord, connectez un fournisseur depuis /integrations : saisissez ses identifiants, activez-le, puis envoyez un événement de test.
- Créez un topic depuis /topics et rattachez-y les intégrations qu'il doit atteindre.
- Publiez sur le topic avec POST /v1/notify/send.
Types d'événements et fournisseurs
| Bloc d'événement | Fournisseurs |
|---|---|
messaging |
Slack, Telegram |
email |
Amazon SES, Resend, Mailchimp, Mailgun |
sms |
Amazon SNS |
mobile_push |
Firebase |
web_push |
Pusher |
webhook |
Webhook (votre propre endpoint HTTP), API personnalisée (votre URL, vos en-têtes et votre corps) |
Les identifiants des fournisseurs restent dans le tableau de bord, chiffrés. Ils ne passent jamais dans vos requêtes API, qui ne transportent que le contenu de l'événement.
Publier sur un topic #
POST /v1/notify/sendCet appel met l'événement en file pour chaque intégration rattachée au topic et répond tout de suite. La livraison sur chaque canal se fait ensuite de façon asynchrone. Utilisez la même clé API que pour les SMS.
Paramètres du corps
| Paramètre | Type | Requis | Description |
|---|---|---|---|
topic |
string | Oui | Slug d'un topic actif (1 à 255 caractères). |
messaging
email
sms
mobile_push
web_push
webhook
|
object | array | Au moins un | Blocs d'événement. Chacun est un objet, ou un tableau d'objets si vous voulez en envoyer plusieurs. |
Champs des blocs d'événement
| Bloc | Champs requis | Champs optionnels |
|---|---|---|
messaging |
platform, content | Slack: channel, username, icon_emoji, icon_url, thread_ts, blocks, attachments. Telegram: chat_id, parse_mode, disable_notification, disable_web_page_preview, message_thread_id, reply_to_message_id |
email |
to, body | subject, text, from, reply_to, cc, bcc, tags, provider |
sms |
to, message | sender_id, sms_type, provider |
mobile_push |
device_token, message | title, priority, collapse_key, provider |
web_push |
channels, event | data, socket_id, provider |
webhook |
— | provider |
À savoir
platformest requis dans un blocmessaging:slackoutelegram.- Si le topic a plusieurs intégrations pour le même événement, ajoutez
providerà n'importe quel autre bloc (par exemple"resend") pour n'atteindre que ce fournisseur. bodyest le contenu HTML.subjectvaut « Notification » par défaut.toest un numéro international (E.164).sms_typevautTransactionalouPromotional.- Un bloc
webhookpart tel quel vers votre endpoint, comme corps JSON, avec la méthode et les en-têtes définis sur l'intégration. - Pour une intégration API personnalisée, le bloc
webhookcontient les données : écrivez${data.order_id}dans l'URL, les en-têtes ou le corps de l'intégration et chaque appel le remplit.${topic},${message_id},${timestamp}sont aussi disponibles. Dans le corps, une variable seule entre ses guillemets garde son type (nombre, true/false, objet). - Les champs des blocs sont vérifiés à la livraison, pas ici. Un bloc auquel il manque un champ requis est accepté par cet appel, puis sa livraison est enregistrée en échec.
Exemple de requête
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/notify/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ' + credentials
},
body: JSON.stringify({
topic: 'orders',
email: {
to: '[email protected]',
subject: 'Order #4821 shipped',
body: '<p>Your order #4821 is on its way.</p>'
},
messaging: {
platform: 'slack',
content: 'Order #4821 shipped'
},
sms: {
to: '+22507XXXXXXXX',
message: 'Your order #4821 is on its way.'
}
})
});
if (!res.ok) {
// 400, 401, 429…: the body carries the reason
const { message } = await res.json();
throw new Error(`Notify refused (${res.status}): ${message}`);
}
const data = await res.json();
$ch = curl_init('https://api.callistosignal.com/v1/notify/send');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'topic' => 'orders',
'email' => [
'to' => '[email protected]',
'subject' => 'Order #4821 shipped',
'body' => '<p>Your order #4821 is on its way.</p>',
],
'messaging' => [
'platform' => 'slack',
'content' => 'Order #4821 shipped',
],
'sms' => [
'to' => '+22507XXXXXXXX',
'message' => 'Your order #4821 is on its way.',
],
]),
]);
$response = curl_exec($ch);
curl_close($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
-X POST https://api.callistosignal.com/v1/notify/send \
-H 'Content-Type: application/json' \
-d '{
"topic": "orders",
"email": {
"to": "[email protected]",
"subject": "Order #4821 shipped",
"body": "<p>Your order #4821 is on its way.</p>"
},
"messaging": {
"platform": "slack",
"content": "Order #4821 shipped"
},
"sms": {
"to": "+22507XXXXXXXX",
"message": "Your order #4821 is on its way."
}
}'
Réponse
{
"status": "queued",
"topic": "orders",
"queued_events": 3,
"topic_messages": 3
}
Champs de la réponse
status— Toujoursqueued: la livraison est asynchrone.topic— Le topic sur lequel l'événement a été publié.queued_events— Nombre de types de blocs d'événement dans la requête.topic_messages— Nombre de livraisons créées : une par bloc et par intégration correspondante.0signifie qu'aucune intégration du topic ne gère les blocs envoyés.
Requêtes refusées
Rien n'est mis en file si :
topicest absent, vide ou dépasse 255 caractères (400) ;- la requête ne contient aucun bloc d'événement (
400) ; - le topic n'existe pas ou est désactivé (
404) ; - aucune intégration active n'est rattachée au topic (
404).
URL Monitor #
Vérifiez que vos URL, vos ports TCP ou vos serveurs (ping) répondent. Callisto les teste à l'intervalle choisi et vous envoie un e-mail quand ils ne répondent plus.
Configuration
- Connectez-vous à votre tableau de bord.
- Allez dans Uptimes → UpTime → Nouveau moniteur.
- Choisissez la méthode (GET, HEAD, POST, TCP ou PING), saisissez l'URL (hôte:port pour TCP, un hôte seul pour PING) et l'intervalle, de 30 s à 1 h. L'intervalle le plus court dépend de votre plan. Le délai de réponse est fixé à 10 s.
- Si besoin, ajoutez un délai de grâce : l'alerte ne part que si l'échec dure plus longtemps. Une réponse de 200 à 399 compte comme en ligne.
- Choisissez l'adresse e-mail qui recevra les alertes. Par défaut, c'est celle du compte.
Notifications : un e-mail quand le moniteur tombe (erreur, délai dépassé ou code hors de 200 à 399), un autre quand il revient.
Tarif : facturé au mois, par moniteur. Voir les tarifs.
Une API publique arrive. En attendant, configurez vos moniteurs depuis le tableau de bord.
Heartbeat Monitor #
Surveillez vos cron jobs et vos tâches planifiées. À la fin de chaque exécution, votre job envoie un ping HTTP à une URL unique. Si le ping n'arrive pas dans l'intervalle prévu, Callisto vous prévient par e-mail.
Configuration
- Allez dans Uptimes → Heartbeats → Nouveau moniteur.
- Donnez un nom au heartbeat, par exemple « Sauvegarde de nuit » ou « Synchro inventaire ».
- Indiquez à quelle fréquence vous attendez un ping (ex. 5 min) et un délai de grâce (ex. 1 min).
- Copiez l'URL de ping générée. Elle a la forme
https://ping.callistosignal.com/<uuid>. - À la fin de votre cron job, ajoutez un appel HTTP vers cette URL.
Ajouter le ping à votre job
curl -fsS https://ping.callistosignal.com/<uuid>
*/5 * * * * /usr/bin/php /path/to/job.php && curl -fsS https://ping.callistosignal.com/<uuid>
// À la fin de votre job :
file_get_contents('https://ping.callistosignal.com/<uuid>');
Le ping est une requête GET. Un ping valide répond 200 {"ok": true, "monitor": "…", "received_at": "…"}. Un identifiant inconnu renvoie 404. Au-delà de 60 pings par minute depuis la même adresse IP, l'URL répond 429.
Notifications : vous recevez un e-mail si aucun ping n'arrive dans le délai prévu (intervalle + grâce).
Tarif : facturé au mois, par moniteur. Voir les tarifs.
Une API publique pour gérer les heartbeats arrive. En attendant, configurez-les depuis le tableau de bord.
Suivi des bugs #
Envoyez à Callisto les exceptions levées par votre code. Chaque app d'erreurs a son propre DSN : le SDK y envoie les événements, ceux qui ont le même type, le même message et le même emplacement sont regroupés en un seul problème, et vous êtes prévenu par e-mail, SMS ou WhatsApp dès qu'une nouvelle erreur apparaît, avec un résumé chaque semaine.
Configuration
- Allez dans Monitoring → Suivi des bugs → Nouvelle application et choisissez la plateforme.
- Copiez le DSN affiché sur la page de l'app. Il ressemble à
https://ingest.callistosignal.com/apps/<app-id>?key=<key>. - Installez le SDK de votre langage et donnez-lui le DSN, ou envoyez les événements en simple HTTP.
- Levez une erreur de test et vérifiez qu'elle apparaît sur la page de l'app.
Signaler une erreur
// npm install callisto-sdk
import { ErrorReporter } from "callisto-sdk";
const callisto = new ErrorReporter({
dsn: "https://ingest.callistosignal.com/apps/<app-id>?key=<key>",
environment: "production",
});
try {
// your code
} catch (err) {
callisto.captureException(err, "error", { feature: "checkout" });
throw err;
}
// composer require callisto-php/sdk
use Callisto\Sdk\Callisto;
Callisto::init(
dsn: "https://ingest.callistosignal.com/apps/<app-id>?key=<key>",
environment: "production",
captureUnhandled: true,
);
try {
// your code
} catch (\Throwable $e) {
Callisto::captureException($e, "error", ["feature" => "checkout"]);
throw $e;
}
# pip install callisto-sdk
from callisto_sdk import ErrorReporter
callisto = ErrorReporter(
dsn="https://ingest.callistosignal.com/apps/<app-id>?key=<key>",
environment="production",
)
try:
risky()
except Exception as exc:
callisto.capture_exception(exc, level="error", extra={"feature": "checkout"})
raise
curl -X POST "https://ingest.callistosignal.com/apps/<app-id>?key=<key>" \
-H "Content-Type: application/json" \
-d '{
"level": "error",
"type": "RuntimeException",
"message": "Something broke",
"culprit": "app/Service.php:42",
"context": { "environment": "production" }
}'
Il existe aussi des SDK pour Go, Ruby, Java et .NET, et des configurations prêtes pour Laravel, Symfony, BowPHP, React, Vue, Angular et NestJS. La page de l'app affiche l'extrait correspondant à la plateforme choisie.
Champs d'un événement en HTTP
| Paramètre | Type | Requis | Description |
|---|---|---|---|
message |
string | Oui | Ce qui s'est mal passé. Le seul champ requis. |
level |
string | Non | L'une des valeurs fatal, error, warning, info. Toute autre valeur est enregistrée comme error. |
type |
string | Non | La classe de l'exception, par exemple RuntimeException. |
culprit |
string | Non | L'endroit où l'erreur s'est produite, sous la forme fichier:ligne. |
stacktrace |
array | Non | Une liste de frames avec file, line, function. Ajoutez pre_context, context_line et post_context pour afficher le code autour de la ligne. Les SDK envoient 10 lignes de chaque côté. |
context |
object | Non | Tout autre détail, par exemple l'environnement ou la version. |
user |
object | Non | L'utilisateur concerné, par exemple son id et son e-mail. |
request |
object | Non | La requête HTTP en cours, par exemple la méthode et le chemin. |
Réponse
{
"ok": true,
"event_id": "<event-id>"
}
Événements refusés
Rien n'est enregistré lorsque :
- la clé du DSN est incorrecte (
401) ; - l'app est désactivée (
403) ; - l'app n'existe pas (
404) ; - le corps dépasse 512 Kio (
413) ; - le corps n'est pas un JSON valide (
400) ; messageest absent (422) ;- vous envoyez plus de 60 événements par minute à la même app depuis la même adresse IP (
429).
Tarif : facturé au mois, par app. Voir les tarifs.
Logs #
Envoyez les logs de vos applications avec OpenTelemetry (OTLP/HTTP), puis cherchez-les et suivez-les en direct dans Monitoring → Logs. Vous gardez votre SDK OpenTelemetry ou votre Collector : seuls l'endpoint et l'en-tête d'authentification changent.
Offres
Les logs sont inclus dans les offres suivantes :
- Business : 10 Go de logs par mois, conservés 30 jours
- Entreprise : 50 Go de logs par mois, conservés 180 jours
Non inclus dans les offres Discover, Start.
Configuration
- Créez une clé d'envoi dans Monitoring → Logs → Clés d'envoi, ou une clé de scope Logs dans Clés API.
- Copiez la clé d'accès (
pk_…) et le secret (sk_…). Le secret n'est affiché qu'une fois : si vous le perdez, créez une nouvelle clé. - Donnez à votre SDK OpenTelemetry ou à votre Collector l'endpoint et l'en-tête ci-dessous, et un nom de service à chaque application.
- Vos logs apparaissent dans Monitoring → Logs, où vous pouvez les chercher et les suivre en direct.
Une clé Logs sert seulement à envoyer des logs : l'API de messagerie la refuse. À l'inverse, une clé API ordinaire est refusée par l'endpoint des logs.
Une clé, plusieurs applications
Une clé d'envoi suffit pour toutes vos applications ; chaque application est identifiée par service.name.
Avec un SDK OpenTelemetry, donnez ce nom par la variable OTEL_SERVICE_NAME. Avec un Collector, un processeur resource le donne aux logs qui arrivent sans nom. Les extraits plus bas montrent les deux.
Vous pouvez créer d'autres clés, jusqu'à 50 par organisation, pour isoler une application : vous pourrez alors désactiver, remplacer, supprimer sa clé ou purger ses logs sans toucher aux autres. Avec une seule clé, une fuite oblige à la remplacer dans toutes vos applications à la fois.
Sources
- La page Monitoring → Logs → Sources liste les services trouvés dans le
service.namede vos logs. Rien à créer : une source apparaît avec ses premiers logs. Les logs sans nom de service, ou avecunknown_service(la valeur par défaut d'OpenTelemetry), sont regroupés sous « Sans service ». - Depuis cette page, un propriétaire ou un administrateur de l'organisation peut purger les logs d'un service, quelle que soit la clé qui les a envoyés, en saisissant le nom du service pour confirmer. Les logs sont effacés sous 7 jours au plus, et les nouveaux logs du service continuent d'arriver.
- Supprimer une clé l'arrête immédiatement, mais n'efface pas les logs déjà reçus : ils restent jusqu'à la fin de la durée de conservation de votre offre. Pour les effacer plus tôt, purgez la clé, même supprimée (Monitoring → Logs → Clés d'envoi), ou le service concerné.
Endpoint
POST https://logs.callistosignal.com/v1/logs
- Authentification : l'en-tête
Authorization: Basic base64(pk_…:sk_…), avec la clé d'accès et le secret encodés en base64 (HTTP Basic). - Formats :
application/x-protobufouapplication/json(encodage JSON d'OTLP). Le corps peut être compressé avecContent-Encoding: gzip.
Configurer l'envoi
OTEL_SERVICE_NAME=checkout-api
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://logs.callistosignal.com/v1/logs
OTEL_EXPORTER_OTLP_LOGS_HEADERS=Authorization=Basic%20<base64(pk_…:sk_…)>
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/protobuf
receivers:
otlp:
protocols:
http:
processors:
resource/callisto:
attributes:
- key: service.name
value: checkout-api
action: insert
exporters:
otlphttp/callisto:
logs_endpoint: https://logs.callistosignal.com/v1/logs
headers:
Authorization: "Basic <base64(pk_…:sk_…)>"
service:
pipelines:
logs:
receivers: [otlp]
processors: [resource/callisto]
exporters: [otlphttp/callisto]
curl -X POST "https://logs.callistosignal.com/v1/logs" \
-u "pk_…:sk_…" \
-H "Content-Type: application/json" \
-d '{
"resourceLogs": [{
"resource": { "attributes": [
{ "key": "service.name", "value": { "stringValue": "checkout-api" } }
] },
"scopeLogs": [{
"logRecords": [{
"timeUnixNano": "'"$(date +%s)"'000000000",
"severityNumber": 17,
"severityText": "ERROR",
"body": { "stringValue": "payment provider timeout" },
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"attributes": [
{ "key": "order_id", "value": { "stringValue": "ord_8F2KQ" } }
]
}]
}]
}]
}'
Remplacez checkout-api par le nom de chaque application. Dans la variable d'environnement, l'espace après Basic s'écrit %20. Dans le Collector, le processeur resource donne un service.name aux logs qui n'en ont pas : un nom déjà fixé par l'application est gardé. La page de la clé, dans le tableau de bord, affiche ces extraits avec votre clé déjà remplie.
Champs enregistrés
| Paramètre | Description |
|---|---|
timeUnixNano |
L'heure du log. À défaut, observedTimeUnixNano, puis l'heure de réception. |
severityNumber, severityText |
Le niveau, de TRACE à FATAL. Si severityText est vide, il est déduit de severityNumber. |
service.name |
Attribut de la ressource qui nomme le service : chaque nom forme une source. unknown_service s'il manque. |
body |
Le message. Une valeur structurée est enregistrée sous forme de texte. |
traceId, spanId |
Les identifiants de trace et de span, en hexadécimal dans le JSON. |
attributes |
Les attributs de la ressource et du log. Le nom et la version du scope et le nom d'événement sont gardés comme attributs. |
Limites
- Corps : 5 Mo au plus, compressé et une fois décompressé.
- Au plus 50 000 logs par requête.
- Un log daté d'avant la durée de conservation de votre offre, ou de plus d'une heure dans le futur, est refusé et compté dans
partialSuccess. Les autres logs de la requête sont acceptés.
Réponse
{
"partialSuccess": {
"rejectedLogRecords": "2",
"errorMessage": "2 log records rejected: timestamp older than the retention or more than 1 h ahead"
}
}
Si tous les logs sont acceptés, la réponse est un objet vide. Elle suit le format de la requête : JSON ou protobuf.
Requêtes refusées
- le corps est mal formé ou le gzip est invalide (
400) ; - la clé est absente, invalide ou désactivée, ou l'adresse IP n'est pas autorisée par la clé (
401) ; - votre offre n'inclut pas les logs (
403) ; - le corps dépasse 5 Mo, ou la requête contient plus de 50 000 logs (
413) ; - le Content-Type ou le Content-Encoding n'est pas pris en charge (
415).
429 ou 503 : renvoyez la requête après le délai donné par l'en-tête Retry-After. Les exportateurs OpenTelemetry le font d'eux-mêmes.
Données personnelles : n'envoyez pas de données personnelles dans vos logs (e-mails, numéros de téléphone, mots de passe, jetons).
La recherche se fait dans le tableau de bord. Il n'existe pas d'API publique de recherche dans les logs.
Webhook Capture #
Voyez en direct les requêtes HTTP entrantes pour déboguer vos intégrations tierces. Chaque webhook a sa propre URL, et chaque requête qu'il reçoit est enregistrée : en-têtes, corps et paramètres de requête.
Configuration
- Allez dans Monitoring → Webhooks → Nouveau webhook.
- Donnez un nom au webhook, par exemple « Stripe test » ou « Hub2 staging ».
- Copiez l'URL générée. Elle a la forme
https://webhook.callistosignal.com/<token>, où le token fait 32 caractères hexadécimaux. - Déclarez-la comme URL de webhook chez le service tiers que vous voulez tester (Stripe, Hub2, etc.).
- Les requêtes s'affichent en direct dans le tableau de bord, avec leurs en-têtes, leur corps et leurs paramètres de requête.
Méthodes acceptées : GET, POST, PUT, PATCH, DELETE, OPTIONS.
Réponses : une requête enregistrée reçoit 200 {"received": true}. L'URL répond 413 au-delà de 1 Mio, 402 une fois la période payée terminée, 404 si le webhook est en pause ou n'existe pas, et 429 au-delà de 120 requêtes par minute depuis la même adresse IP.
Affichage : chaque requête apparaît dans le tableau de bord dès son arrivée, avec son horodatage, ses en-têtes, son corps et ses paramètres de requête.
Tarif : facturé au mois, par webhook. Voir les tarifs.
Une API publique pour lister les requêtes capturées arrive. En attendant, consultez-les dans le tableau de bord.
Vérifier le solde #
GET /v1/sms/balanceParamètres de requête
| Paramètre | Type | Par défaut | Description |
|---|---|---|---|
format |
string | full | full renvoie aussi les prix du SMS local et international, simple seulement le crédit et la devise. |
currency |
string | — | Devise affichée : XOF, XAF, USD, EUR, NGN, GHS. Par défaut, celle de votre organisation. Elle ne change que le libellé : le crédit n'est pas converti. |
// Consulter le solde du compte
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/sms/balance', {
headers: { 'Authorization': 'Basic ' + credentials }
});
// Consulter le solde du compte
$ch = curl_init('https://api.callistosignal.com/v1/sms/balance');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
https://api.callistosignal.com/v1/sms/balance
Réponse
{
"credit": 975,
"currency": "XOF",
"sms_price_local": 25,
"sms_price_international": 50
}
Statut d'un message #
GET /v1/sms/{id}// Consulter le statut d'un message
const credentials = btoa('ACCESS_KEY:ACCESS_SECRET');
const res = await fetch('https://api.callistosignal.com/v1/sms/550e8400-e29b-41d4-a716-446655440000', {
headers: { 'Authorization': 'Basic ' + credentials }
});
// Consulter le statut d'un message
$ch = curl_init('https://api.callistosignal.com/v1/sms/550e8400-e29b-41d4-a716-446655440000');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => 'ACCESS_KEY:ACCESS_SECRET',
]);
$response = curl_exec($ch);
curl -u 'ACCESS_KEY:ACCESS_SECRET' \
https://api.callistosignal.com/v1/sms/550e8400-e29b-41d4-a716-446655440000
Un message introuvable renvoie 400.
Statuts possibles
| Statut | Description |
|---|---|
| pending | Le message est enregistré et attend d'être pris en charge |
| initialized | Le message est pris en charge pour l'envoi |
| processing | Le message est en cours d'envoi à l'opérateur |
| processed | L'opérateur a accepté le message |
| failed | L'opérateur a refusé le message, ou toutes les tentatives d'envoi ont échoué |
Codes d'erreur #
| Code | Description |
|---|---|
400 |
Requête invalide. Vérifiez vos paramètres. |
401 |
Non autorisé. La clé API est absente, invalide ou désactivée, ou votre adresse IP n'est pas autorisée. Un en-tête Authorization mal formé renvoie 400. |
402 |
Crédits insuffisants pour un envoi payant (SMS, OTP par SMS). Rien n'est débité. |
404 |
Ressource introuvable |
429 |
Trop de requêtes : vous avez atteint la limite |
500 |
Erreur interne du serveur |
Une erreur renvoie {"message", "code", "trace"}. En production, message vaut « An internal error occurred » pour la plupart des erreurs : fiez-vous au statut HTTP et au champ code (VALIDATION_ERROR, UNAUTHORIZED, INSUFFICIENT_CREDITS, NOT_FOUND…). Les refus pour crédits insuffisants (402), ressource inconnue lors d'un envoi (404), nom d'expéditeur inconnu, numéro de destinataire invalide ou pays non pris en charge, ou organisation sans pays (400) gardent leur vrai message.
Exception : les endpoints OTP renvoient leurs erreurs 400 et 404 sous la forme {"message"}, avec le message réel.
Crédits insuffisants : POST /v1/sms/send et POST /v1/otp/send (SMS) renvoient 402. Rien n'est débité ni créé. Le corps du SMS porte "code": "INSUFFICIENT_CREDITS" ; celui de l'OTP est {"message"}. Les envois WhatsApp et Notify ne prennent pas de crédits à l'appel.
Limites de requêtes #
Pour garder le service stable, ces limites s'appliquent :
- 30 requêtes/minute sur les endpoints qui envoient (SMS, OTP, WhatsApp, Notify)
- 60 requêtes/minute sur les endpoints de lecture
- Les limites sont comptées par adresse IP et par chemin exact. Au-delà, l'API répond 429 avec un en-tête Retry-After.
Les en-têtes de réponse indiquent où vous en êtes par rapport à vos limites :
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 29
Serveur MCP pour les assistants IA #
Cette documentation est aussi disponible pour les assistants IA (Claude, Cursor et tout client compatible) par le Model Context Protocol. L'assistant lit les pages à jour au lieu de deviner les endpoints et les paramètres.
- Adresse :
https://callistosignal.com/mcp - Transport HTTP (Streamable HTTP), JSON-RPC 2.0 : un message par requête POST, une réponse JSON.
- Lecture seule, sans clé ni authentification : le serveur ne donne accès qu'à la documentation publique, jamais à votre compte ni à vos données.
Connecter un assistant
Avec Claude Code :
claude mcp add --transport http callisto-docs https://callistosignal.com/mcp
Avec un autre client (Cursor, par exemple), déclarez un serveur MCP HTTP avec cette adresse :
{
"mcpServers": {
"callisto-docs": {
"url": "https://callistosignal.com/mcp"
}
}
}
Outils
| Outil | Paramètre | Description |
|---|---|---|
list_sections |
lang |
Liste les sections de la documentation, avec leur identifiant et leur titre. |
get_section |
id, lang |
Renvoie le texte complet d'une section en Markdown : endpoints, paramètres, exemples, erreurs. |
search_docs |
query, lang, limit |
Cherche les sections qui contiennent tous les mots de la requête, avec un court extrait de chacune. |
lang vaut fr (par défaut) ou en. id est l'identifiant donné par list_sections, par exemple otp-send. limit va de 1 à 25 (5 par défaut).
Limites
- 60 requêtes/minute par client et 600/minute pour l'ensemble des clients. Au-delà, le serveur répond 429 avec un en-tête Retry-After et une erreur JSON-RPC (code -32000).
- Un seul message JSON-RPC par requête (pas de lot), de 64 Ko au plus.
- Seul POST est accepté : GET et les autres méthodes répondent 405 (pas de flux SSE, pas de session).