API ET WEBHOOKS
API et webhooks de Vortex
Lisez vos prospects, clients et analytics depuis vos propres outils, et recevez un appel signé quand il se passe quelque chose dans votre espace.
Inclus dans le plan Agency. Lecture seule : l'API ne peut rien créer, modifier ni supprimer.
Authentification
Créez une clé dans Réglages, API et webhooks. Elle ressemble à vtx_ suivi de 8 caractères, d'un tiret bas et de 32 caractères. Elle n'est affichée qu'une fois : Vortex n'en garde qu'une empreinte, conservez-la dans votre gestionnaire de mots de passe. Envoyez-la dans l'en-tête Authorization.
curl https://usevortex.tech/api/v1/prospects \
-H "Authorization: Bearer vtx_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"- Une clé appartient à un seul espace de travail et ne peut lire que cet espace.
- Une clé révoquée cesse de fonctionner immédiatement. La révocation marche sur tous les plans.
- Les clés ont des portées. L'API accepte les clés de portée api:read.
- Si l'espace quitte le plan Agency ou que son abonnement s'arrête, chaque requête est refusée avec le code 403.
Limites
- 60 requêtes par minute et par clé, par fenêtres fixes d'une minute. Au-delà, vous recevez 429 avec un en-tête Retry-After en secondes. Chaque réponse réussie porte X-RateLimit-Limit.
- Les listes renvoient 25 éléments par défaut et au plus 100 (paramètre limit).
- Jusqu'à 10 clés actives et 5 adresses de webhook par espace de travail.
Réponses et erreurs
Succès : { "data": ... }. Les listes ajoutent "next_cursor" (null sur la dernière page). Erreurs : { "error": { "code": "...", "message": "..." } }.
| Statut | Code | Signification |
|---|---|---|
| 400 | invalid_limit, invalid_cursor, invalid_status, invalid_days | Un paramètre n'est pas valide |
| 401 | invalid_key | Clé absente, mal formée, inconnue ou révoquée (toujours la même réponse) |
| 403 | plan_required, subscription_inactive, insufficient_scope, feature_unavailable | La clé est valide mais l'espace ne peut pas l'utiliser |
| 404 | not_found | Aucun objet de ce type dans votre espace |
| 429 | rate_limited | Trop de requêtes |
Points d'accès
Tous les points d'accès utilisent GET et commencent par https://usevortex.tech/api/v1.
| Point d'accès | Paramètres | Renvoie |
|---|---|---|
| GET /api/v1 | - | Vérifie la clé et décrit l'API |
| GET /api/v1/prospects | status, limit, cursor | Les prospects, du plus récent au plus ancien |
| GET /api/v1/prospects/{id} | - | Un prospect |
| GET /api/v1/clients | limit, cursor | Les clients : id, prospect_id, website_url |
| GET /api/v1/clients/{id}/analytics | days (1-90) | Les chiffres agrégés du portail, déjà collectés |
| GET /api/v1/payments | status, limit, cursor | Les paiements de vos clients encaissés via Stripe Connect |
Valeurs de statut d'un prospect : new, active, standby, client, blacklisted. Valeurs de statut d'un paiement : paid, pending, expired, void, uncollectible, open. Repassez next_cursor dans cursor pour obtenir la page suivante.
{
"data": [
{ "id": "…", "status": "new", "city": "Lyon", "niche": "Bakery", "siren": "123456789", "created_at": "2026-10-01T10:00:00.000Z" }
],
"next_cursor": "eyJhdCI6…"
}{ "data": { "client_id": "…", "period": { "days": 30, "from": "…", "to": "…" },
"visitors": 120, "pageviews": 340, "form_submits": 6, "call_clicks": 11,
"conversion_rate_pct": 5, "avg_load_time_ms": 900, "truncated": false } }Les paiements sont ceux que vos clients vous ont versés via Vortex (amount_cents, application_fee_cents, currency, status, created_at). Les prospects d'exemple montrés aux nouveaux comptes ne sont pas renvoyés.
Champs jamais renvoyés
Le contenu issu de Google Places n'est exposé ni par l'API ni par les webhooks : name, address, phone, hours, rating, reviews, google_maps_url. Le nom d'entreprise d'un client n'est pas exposé non plus. Les adresses e-mail, les liens de portail et les liens de facture ne sont jamais renvoyés.
Webhooks
Ajoutez une adresse dans Réglages, API et webhooks, et choisissez ses événements. Vortex envoie alors un POST avec un corps JSON quand l'événement se produit. Le secret de signature (whsec_…) n'est affiché qu'une fois, à l'ajout de l'adresse.
- https:// uniquement, le port par défaut et un vrai nom de domaine (ni adresse IP, ni localhost, ni nom interne).
- Le nom doit pointer vers des adresses publiques. C'est revérifié à chaque envoi, et la connexion va à l'adresse vérifiée.
- Les redirections ne sont pas suivies.
Événements et contenu
| Événement | Quand |
|---|---|
| prospect.created | Un prospect est ajouté, à la main ou depuis une recherche |
| prospect.status_changed | Un prospect passe à une autre étape du pipeline (previous_status est inclus) |
| payment.received | Un paiement client est réglé via Stripe Connect |
| webhook.test | Seulement quand vous cliquez sur « Envoyer un test » |
{
"id": "6f1c1d5e-6b0e-4d0b-9a3a-0e5a1b2c3d4e",
"type": "prospect.created",
"created": "2026-10-23T10:00:00.000Z",
"data": { "id": "…", "status": "new", "city": "Lyon", "niche": "Bakery", "siren": null, "created_at": "…" }
}id est l'identifiant de l'événement : un même événement n'est jamais livré deux fois à une même adresse, et l'identifiant reste le même lors des reprises, vous pouvez donc l'utiliser pour dédoublonner.
| En-tête | Valeur |
|---|---|
| Vortex-Event-Id | L'identifiant de l'événement |
| Vortex-Event-Type | Le type d'événement |
| Vortex-Delivery-Attempt | 1 pour le premier essai |
| Vortex-Signature | t=<unix seconds>,v1=<hex> |
Vérifier la signature
v1 vaut HMAC-SHA256(secret, "<t>.<corps brut de la requête>") en hexadécimal. Calculez-le sur le corps brut, comparez en temps constant et rejetez les horodatages de plus de 5 minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, header, rawBody, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = Buffer.from(parts.v1 ?? "");
const wanted = Buffer.from(expected);
return given.length === wanted.length && timingSafeEqual(given, wanted);
}import hashlib, hmac, time
def verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts.get("v1", ""), expected)Envoi, reprises et échecs
- Répondez par un code 2xx dans les 10 secondes. Tout le reste, redirection comprise, est un échec.
- Un envoi en échec est repris après 1 min, 5 min, 30 min, 2 h, 6 h (6 essais au total). Le calendrier est stocké en base, il survit aux redémarrages.
- Après 10 échecs de suite sur une même adresse, elle est désactivée et la raison s'affiche dans les Réglages. La réactiver remet le compteur à zéro.
- Le journal des envois, dans les Réglages, garde les 30 derniers jours : date, événement, résultat, essais et code HTTP ou code d'erreur court. La réponse de votre serveur n'est jamais enregistrée.