Développeurs

API REST et webhooks Appelable

Vérifiez un numéro, synchronisez les accords et journalisez les appels depuis vos outils. API et webhooks inclus dans Cabinet et Réseau.

Essayer gratuitement

Démarrage

URL de base
https://tuzanhultqcubdubhojy.supabase.co/functions/v1/api
Limite
600 requêtes/minute/clé
Authentification
En-tête Authorization: Bearer apl_live_… — clé créée dans Intégrations.

Erreurs

Format : { "error": { "code", "message" } }. Codes possibles : 400 invalid_request, invalid_phone ou invalid_outcome ; 401 clé invalide ; 402 subscription_required ; 403 plan_not_allowed ; 404 ressource introuvable ; 429 limite dépassée.

Routes

GET

/v1/check?phone=+33612345678

Vérifie immédiatement si un numéro peut être appelé.

curl "https://tuzanhultqcubdubhojy.supabase.co/functions/v1/api/v1/check?phone=%2B33612345678" \
  -H "Authorization: Bearer apl_live_…"

Réponse

{
  "phone": "+33612345678",
  "callable": true,
  "reason": "ok",
  "reason_label": "Accord actif",
  "consent_id": "uuid",
  "expires_at": "2027-09-25T09:00:00Z",
  "calls_last_30d": 1,
  "within_legal_hours": true,
  "next_window": null
}

callable tient compte de l'accord, de l'opposition, du refus de moins de 60 jours et du quota de 4 appels sur 30 jours ; within_legal_hours indique si l'heure actuelle est dans les horaires légaux, next_window donne le prochain créneau autorisé sinon.

Motifs : ok, pas_de_consentement_actif, consentement_expire, opposition, refus_moins_de_60_jours, quota_4_appels_30_jours, numero_invalide.

POST

/v1/check

Vérifie jusqu'à 1 000 numéros en une requête.

curl -X POST "https://tuzanhultqcubdubhojy.supabase.co/functions/v1/api/v1/check" \
  -H "Authorization: Bearer apl_live_…" -H "Content-Type: application/json" \
  -d '{"phones":["06 12 34 56 78","+33687654321"]}'

Réponse

{
  "count": 2,
  "callable": 1,
  "results": [
    { "input": "06 12 34 56 78", "phone": "+33612345678", "callable": true,
      "reason": "ok", "reason_label": "Accord actif", "consent_id": "uuid" }
  ]
}
GET

/v1/consents

Liste les accords avec filtres et pagination par curseur. Le paramètre status accepte active, pending, withdrawn et expired ; chaque élément suit le format complet ci-dessous.

curl "https://tuzanhultqcubdubhojy.supabase.co/functions/v1/api/v1/consents?status=active&updated_since=2026-09-01T00:00:00Z&limit=100" \
  -H "Authorization: Bearer apl_live_…"

Réponse

{ "data": [{
  "id": "uuid",
  "status": "active",
  "phone": "+33612345678",
  "first_name": "Camille",
  "last_name": "Martin",
  "email": "camille@example.fr",
  "beneficiary": "Votre société",
  "channel": "web",
  "collected_at": "2026-09-25T08:58:00Z",
  "confirmed_at": "2026-09-25T09:00:00Z",
  "expires_at": "2027-09-25T09:00:00Z",
  "withdrawn_at": null,
  "proof_url": "https://appelable.fr/preuve/…"
}], "next_cursor": "…" }
GET

/v1/consents/{id}

Retourne le détail complet d'un accord.

curl "https://tuzanhultqcubdubhojy.supabase.co/functions/v1/api/v1/consents/CONSENT_ID" -H "Authorization: Bearer apl_live_…"

Réponse

{
  "id": "uuid",
  "status": "active",
  "phone": "+33612345678",
  "first_name": "Camille",
  "last_name": "Martin",
  "email": "camille@example.fr",
  "beneficiary": "Votre société",
  "channel": "web",
  "collected_at": "2026-09-25T08:58:00Z",
  "confirmed_at": "2026-09-25T09:00:00Z",
  "expires_at": "2027-09-25T09:00:00Z",
  "withdrawn_at": null,
  "proof_url": "https://appelable.fr/preuve/…"
}
POST

/v1/consents/{id}/withdraw

Retire un accord et enregistre la note associée. La réponse reprend le même objet complet ; si l’accord était déjà retiré, elle contient aussi "already": true.

curl -X POST "https://tuzanhultqcubdubhojy.supabase.co/functions/v1/api/v1/consents/CONSENT_ID/withdraw" \
  -H "Authorization: Bearer apl_live_…" -H "Content-Type: application/json" -d '{"note":"Demande reçue par téléphone"}'

Réponse

{
  "id": "uuid",
  "status": "withdrawn",
  "phone": "+33612345678",
  "first_name": "Camille",
  "last_name": "Martin",
  "email": "camille@example.fr",
  "beneficiary": "Votre société",
  "channel": "web",
  "collected_at": "2026-09-25T08:58:00Z",
  "confirmed_at": "2026-09-25T09:00:00Z",
  "expires_at": "2027-09-25T09:00:00Z",
  "withdrawn_at": "2026-09-25T09:00:00Z",
  "proof_url": "https://appelable.fr/preuve/…"
}
POST

/v1/calls

Enregistre une tentative pour le compteur de 4 appels sur 30 jours et le refus pendant 60 jours.

curl -X POST "https://tuzanhultqcubdubhojy.supabase.co/functions/v1/api/v1/calls" \
  -H "Authorization: Bearer apl_live_…" -H "Content-Type: application/json" \
  -d '{"phone":"+33612345678","outcome":"answered","duration_seconds":95,"notes":"Échange terminé","called_at":"2026-09-25T09:00:00Z"}'

Réponse HTTP 201

{ "id": "uuid", "called_at": "2026-09-25T09:00:00Z", "consent_id": "uuid ou null" }

Valeurs de outcome : answered, no_answer, busy, refused, callback_requested, voicemail, wrong_number. Vous pouvez transmettre phone ou consent_id.

Webhooks signés

Les tests utilisent le type ping avec un id UUID. Les événements métier utilisent un identifiant numérique :

{
  "id": 18342,
  "type": "consent.confirmed",
  "created_at": "2026-09-25T09:00:00Z",
  "data": {
    "consent_id": "uuid", "status": "active", "phone": "+33612345678",
    "first_name": "Camille", "last_name": "Martin", "email": "camille@example.fr",
    "beneficiary": "Votre société", "channel": "web",
    "collected_at": "…", "confirmed_at": "…", "expires_at": "…",
    "withdrawn_at": null, "proof_url": "https://appelable.fr/preuve/…"
  }
}

Dédoublonnez les traitements sur id : un même événement peut être renvoyé en cas de réessai.

Vérifier la signature en Node.js

L'en-tête Appelable-Signature vaut t=<unix>,v1=<hex>. La signature est HMAC-SHA256 du secret appliqué à « timestamp.corps brut ». Rejetez les requêtes de plus de 300 secondes.

import crypto from "node:crypto";

// signature = req.headers["appelable-signature"]
// secret = le whsec_ du webhook
// rawBody = le corps brut non reparsé (par exemple avec express.raw())
const [tPart, v1Part] = signature.split(",");
const timestamp = Number(tPart.replace("t=", ""));
if (Math.abs(Date.now() / 1000 - timestamp) > 300) throw new Error("Signature expirée");
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const received = v1Part.replace("v1=", "");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) throw new Error("Signature invalide");

Réessais automatiques après 1, 5, 30, 120 et 720 minutes, soit 6 tentatives au total. Toute réponse 2xx vaut succès.

Relier votre CRM

Automatisez la synchronisation via Make, Zapier ou n8n. Il ne s'agit pas d'une intégration native.

  1. 1. Déclencheur : dans Make, choisissez « Webhooks → Custom webhook » ; dans Zapier, « Webhooks by Zapier → Catch Hook » ; dans n8n, le nœud « Webhook ». Collez l'URL dans Appelable → Intégrations → Webhooks, cochez consent.confirmed et consent.withdrawn, puis cliquez sur « Tester ».
  2. 2. Recherche : dans HubSpot, utilisez « Search objects / Find contact » pour retrouver le contact par téléphone, puis créez ou mettez à jour la fiche.
  3. 3. Champs personnalisés : créez « Accord d'appel jusqu'au » (date) = data.expires_at, « Preuve d'accord » (URL) = data.proof_url et « Statut accord » = data.status.
  4. 4. Retrait : pour consent.withdrawn, passez « Statut accord » à « retiré » et videz « Accord d'appel jusqu'au ».
  5. 5. Avant une campagne : exportez la liste du CRM, envoyez-la à POST /v1/check par lots de 1 000 numéros, puis ne gardez que les résultats avec callable = true.