Aller au contenu
FR EN
Créer un compte

Docs

Comment envoyer des SMS, des codes OTP et des messages WhatsApp avec l'API Callisto, publier des événements vers vos intégrations et configurer le monitoring.

Démarrage rapide #

Trois étapes, et vous envoyez votre premier message.

  1. 1

    Créer un compte

    Inscrivez-vous sur Callisto. C'est gratuit.

  2. 2

    Obtenir une clé API

    Créez-la depuis votre tableau de bord.

  3. 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/send

Paramè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/send

Envoyez 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/messages

Listez 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
POST /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
POST /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}
GET /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
GET /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

  1. Connectez-vous à votre tableau de bord.
  2. Allez dans WhatsApp → Instances → Nouvelle instance.
  3. Donnez un nom à l'instance, par exemple « Production » ou « Support ».
  4. Sur le téléphone qui hébergera l'instance, ouvrez WhatsApp ou WhatsApp Business et scannez le QR code affiché à l'écran.
  5. Dès que l'instance affiche status: connected, vous pouvez envoyer des messages par l'API.
L'association se fait depuis le tableau de bord. Une fois l'instance connectée, l'API prend le relais pour l'envoi des messages et le suivi de leur statut.
Créer une instance

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/text

Paramètres du corps

Paramètre Type Requis Description
tostringOuiNuméro du destinataire au format E.164 (ex. +22507XXXXXXXX).
messagestringOuiLe texte à envoyer (4096 caractères au plus).
scheduled_atdatetimeNonEnvoyer plus tard, à cette date et heure (YYYY-MM-DD HH:MM:SS).
skip_duplicatedbooleanNonSi 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/media

Paramètres du corps

Paramètre Type Requis Description
tostringOuiNuméro du destinataire (E.164).
typestringOuiimage, video, document ou audio.
media_urlstringOuiURL publique du fichier (http:// ou https://).
captionstringNonLégende affichée avec le média (max 1024 caractères).
filenamestringNonNom du fichier, utile pour document (max 255 caractères).
scheduled_atdatetimeNonEnvoyer plus tard, à cette date et heure (YYYY-MM-DD HH:MM:SS).
skip_duplicatedbooleanNonSi 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/location

Paramètres du corps

Paramètre Type Requis Description
tostringOuiNuméro du destinataire (E.164).
latitudenumberOuiLatitude entre -90 et 90.
longitudenumberOuiLongitude entre -180 et 180.
namestringNonNom du lieu (≤ 255 caractères).
addressstringNonAdresse postale (≤ 1024 caractères).
scheduled_atdatetimeNonEnvoyer plus tard, à cette date et heure (YYYY-MM-DD HH:MM:SS).
skip_duplicatedbooleanNonSi 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}/messages

Paramètres de requête

Paramètre Type Par défaut Description
pageint1Numéro de page (1 par défaut).
per_pageint15Éléments par page, 100 au plus.
started_atdatetime—Début / fin de la période (YYYY-MM-DD HH:MM:SS). Sans dates, seuls les messages du jour sont renvoyés.
ended_atdatetime—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

  1. Dans le tableau de bord, connectez un fournisseur depuis /integrations : saisissez ses identifiants, activez-le, puis envoyez un événement de test.
  2. Créez un topic depuis /topics et rattachez-y les intégrations qu'il doit atteindre.
  3. 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/send

Cet 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

  • platform est requis dans un bloc messaging : slack ou telegram.
  • 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.
  • body est le contenu HTML. subject vaut « Notification » par défaut.
  • to est un numéro international (E.164). sms_type vaut Transactional ou Promotional.
  • Un bloc webhook part 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 webhook contient 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 — Toujours queued : 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. 0 signifie qu'aucune intégration du topic ne gère les blocs envoyés.

Requêtes refusées

Rien n'est mis en file si :

  • topic est 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

  1. Connectez-vous à votre tableau de bord.
  2. Allez dans Uptimes → UpTime → Nouveau moniteur.
  3. 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.
  4. 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.
  5. 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

  1. Allez dans Uptimes → Heartbeats → Nouveau moniteur.
  2. Donnez un nom au heartbeat, par exemple « Sauvegarde de nuit » ou « Synchro inventaire ».
  3. Indiquez à quelle fréquence vous attendez un ping (ex. 5 min) et un délai de grâce (ex. 1 min).
  4. Copiez l'URL de ping générée. Elle a la forme https://ping.callistosignal.com/<uuid>.
  5. À 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

  1. Allez dans Monitoring → Suivi des bugs → Nouvelle application et choisissez la plateforme.
  2. Copiez le DSN affiché sur la page de l'app. Il ressemble à https://ingest.callistosignal.com/apps/<app-id>?key=<key>.
  3. Installez le SDK de votre langage et donnez-lui le DSN, ou envoyez les événements en simple HTTP.
  4. 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) ;
  • message est 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

  1. Créez une clé d'envoi dans Monitoring → Logs → Clés d'envoi, ou une clé de scope Logs dans Clés API.
  2. 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é.
  3. Donnez à votre SDK OpenTelemetry ou à votre Collector l'endpoint et l'en-tête ci-dessous, et un nom de service à chaque application.
  4. 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.name de vos logs. Rien à créer : une source apparaît avec ses premiers logs. Les logs sans nom de service, ou avec unknown_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-protobuf ou application/json (encodage JSON d'OTLP). Le corps peut être compressé avec Content-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

  1. Allez dans Monitoring → Webhooks → Nouveau webhook.
  2. Donnez un nom au webhook, par exemple « Stripe test » ou « Hub2 staging ».
  3. 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.
  4. Déclarez-la comme URL de webhook chez le service tiers que vous voulez tester (Stripe, Hub2, etc.).
  5. 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/balance

Paramè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).