Une requête HTTP POST, trois en-têtes, un tableau de destinataires. Ce guide donne le code réel — cURL, PHP natif, Guzzle, Laravel — la référence des champs, les codes d'erreur et la réception des accusés de réception par webhook.
Autant le dire tout de suite : Conexteo ne publie pas de bibliothèque PHP officielle, et n'en publie pas non plus pour Laravel. Ce n'est pas un manque à combler, c'est un choix qui vous arrange. L'API est une REST classique en JSON sur HTTPS : un client HTTP que vous avez déjà — l'extension cURL de PHP, Guzzle, le client Symfony, `Http` de Laravel — suffit à tout faire. Vous n'ajoutez aucune dépendance à maintenir, aucune couche d'abstraction à comprendre, et vous restez libre de changer de route SMS sans réécrire votre code métier.
Une seule intégration packagée existe côté PHP, et elle est pour Symfony : le bridge connected-company/conexteo-notifier, sous licence MIT, qui branche Conexteo sur le composant Notifier. On y revient plus bas.
Créez un compte sur app.conexteo.com, puis ouvrez Mon Compte / API / Webhook. Vous y trouvez deux valeurs distinctes, et c'est important : un App ID et une clé API. Les deux voyagent dans des en-têtes séparés, sur chaque requête.
Le piège qui coûte le plus d'heures : intervertir les deux valeurs. L'API répond alors 401 Unauthorized — exactement le même code que si la clé était fausse ou révoquée. On cherche donc du côté de la clé, on en régénère une, et le problème persiste. Le moyen mnémotechnique : APP identifie l'application, KEY est le secret. Si vous obtenez un 401 alors que vos identifiants sont fraîchement copiés, échangez les deux en-têtes avant toute autre hypothèse.
Avant d'écrire une seule ligne de PHP, envoyez un message depuis un terminal. Cela isole la question « mes identifiants fonctionnent-ils ? » de la question « mon code est-il correct ? », et c'est dix secondes de gagnées sur chaque diagnostic ultérieur.
La réponse tient en quatre champs :
Chiffrer un envoi sans le déclencher. Ajoutez le paramètre de requête ?resumeBefore=true : l'API retourne le nombre de crédits que la campagne consommerait, sans envoyer un seul message. C'est le moyen propre de valider une segmentation ou d'afficher un coût estimé à un utilisateur avant confirmation.
Sans aucune dépendance, sur n'importe quel hébergement mutualisé :
Deux détails qui font échouer un envoi sur trois en recette. L'expéditeur est limité à 11 caractères — « MaBoutiqueEnLigne » sera refusé, pas tronqué. Et l'encodage : sans JSON_UNESCAPED_UNICODE, les accents partent en séquences d'échappement qui gonflent inutilement le message et peuvent le faire basculer sur deux segments facturés.
Laravel embarque déjà tout ce qu'il faut avec le client Http. Le seul vrai conseil d'architecture : n'appelez jamais l'API depuis un contrôleur. Un envoi de SMS est un appel réseau sortant ; s'il rame, c'est la requête de votre utilisateur qui rame. Encapsulez-le dans un service, déclenchez-le depuis une Job en file d'attente.
config/services.php
app/Services/Conexteo.php
app/Jobs/SendSms.php
Le 409 n'est pas une panne, c'est une protection — et il se traite à l'envers de l'intuition. Le champ external_id est votre identifiant de déduplication. Si un message a déjà été soumis avec la même valeur sur votre compte, l'API répond 409 Conflict — « External ID already processed » — sans renvoyer le message d'origine.
Le scénario qui piège : votre appel expire côté réseau, votre Job est rejouée, l'API répond 409, votre code lève une exception, la Job échoue et vous croyez que le client n'a rien reçu. Il a reçu le message. C'est pour cela qu'un 409 doit être traité comme un succès idempotent, jamais comme une erreur. Sans external_id, le même incident produit deux SMS facturés au même destinataire.
C'est la seule intégration PHP packagée. Elle branche Conexteo sur le composant Notifier de Symfony, ce qui vous permet d'envoyer un SMS avec la même API que vos autres canaux de notification. PHP 8.1 minimum, licence MIT.
Le code source est public : github.com/connected-company/conexteo-notifier. Notez que le DSN reprend la même paire App ID / clé API que les en-têtes HTTP — le piège de l'interversion s'applique donc aussi ici, en première et seconde position du DSN.
| Champ | Requis | Description |
|---|---|---|
| content | oui | Le texte du message. |
| recipients | oui | Tableau de numéros, au format national ou international : ["0606060606", "+33606060606"]. Jusqu'à un million d'entrées. |
| sender | non | Expéditeur alphanumérique, 11 caractères maximum. |
| external_id | non | Votre identifiant de déduplication. Une seconde soumission avec la même valeur renvoie un 409. |
| scheduleAt | non | Horodatage d'envoi en UTC, ISO 8601 — 2026-09-15T08:00:00Z. Absent = envoi immédiat. |
| shorturl | non | Raccourcissement de lien : {"mode":"unique","url":"…"}. Le mode unique génère un lien par destinataire, donc un suivi des clics individuel. |
Le champ scheduleAt attend de l'UTC. Un envoi programmé depuis un serveur réglé sur Europe/Paris sans conversion partira avec une ou deux heures d'avance selon la saison. En PHP : (new DateTime('2026-09-15 10:00', new DateTimeZone('Europe/Paris')))->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d\TH:i:s\Z'). En été, une campagne prévue à 8 h partirait à 6 h — hors des plages autorisées pour un message promotionnel.
| Code | Cause la plus probable | Que faire |
|---|---|---|
| 200 | Message accepté et mis en file. | Conserver message_id : c'est la clé de rapprochement des webhooks. |
| 400 | Expéditeur de plus de 11 caractères, recipients envoyé comme chaîne au lieu d'un tableau, ou scheduleAt mal formaté. | Journaliser le corps de la réponse : il nomme le champ fautif. |
| 401 | App ID et clé API intervertis, bien plus souvent qu'une clé invalide. | Échanger les deux en-têtes avant de régénérer quoi que ce soit. |
| 409 | Un message portant le même external_id a déjà été traité. | Traiter comme un succès. Le message est parti ; ne pas rejouer. |
L'envoi n'est que la moitié du travail. Ce qui distingue une intégration jouet d'une intégration de production, c'est de savoir ce qu'est devenu chaque message. Conexteo pousse trois types d'événements sur l'URL que vous déclarez : ack_sms pour les accusés de réception SMS, reply pour les réponses entrantes de vos destinataires — y compris les clics sur les suggestions RCS —, et ack_rcs pour les statuts RCS.
Deux modes existent, et le choix n'est pas anodin. Un webhook non signé est envoyé une seule fois : si votre serveur redémarre au mauvais moment, l'événement est perdu définitivement. Un webhook signé porte une signature HMAC et bénéficie des réessais — jusqu'à huit tentatives échelonnées sur environ dix-huit heures (30 secondes, 2 minutes, 10 minutes, 30 minutes, 1 heure, 4 heures, puis 12 heures). C'est aussi le mode requis pour recevoir les statuts RCS. En production, il n'y a pas vraiment de débat.
En mode signé, chaque requête porte en plus une signature HMAC calculée avec le secret de votre compte — celui qui commence par whsec_. La vérification tient en quelques lignes, et le point important est d'utiliser une comparaison à temps constant plutôt que === :
Deux erreurs classiques sur cette vérification. Calculer l'empreinte sur le tableau décodé plutôt que sur le corps brut : un simple réordonnancement des clés lors du décodage change la signature, et tout échoue sans raison apparente — utilisez toujours $request->getContent(). Et comparer avec ===, qui expose à une attaque temporelle : hash_equals() existe pour cela.
Toute réponse 2xx vaut accusé de réception. Tout autre code, un timeout ou une erreur réseau compte comme une tentative en échec. Répondez donc avant de faire le travail lourd : accusez réception, puis traitez en file d'attente. Un webhook qui attend la fin d'un traitement de huit secondes finit par être considéré en échec.
L'en-tête X-Conexteo-Webhook-Id reste identique entre toutes les tentatives d'un même événement : c'est votre garantie contre les doublons, à condition de le stocker. Et vous pouvez auditer l'ensemble à tout moment avec GET /webhooks/deliveries, qui expose les statuts pending, delivered, failed et cancelled.
Un code de confirmation et une promotion partent par le même endpoint, mais ne relèvent pas du même régime. Les messages promotionnels s'envoient du lundi au vendredi de 8 h à 20 h et le samedi de 10 h à 19 h, hors dimanches et jours fériés — ce sont les recommandations de la CNIL et les usages du secteur, pas un décret, mais elles engagent la réputation de vos routes. Les messages transactionnels, eux, ne sont pas concernés et partent 24 h sur 24. Si votre application mélange les deux flux, séparez-les dès la conception : la route dédiée au SMS transactionnel existe pour qu'un code d'authentification ne se retrouve jamais en file derrière un envoi de masse. Le détail des règles applicables est sur la page RGPD et SMS marketing.
Inscription gratuite, 5 messages de test offerts, sans carte bancaire. L'App ID et la clé API sont disponibles immédiatement après la création du compte.
Créer un compte gratuitement →