API WEBFUL v1

API REST pour lire vos donnees analytics WEBFUL depuis n'importe quelle application, script ou automation (n8n, Make, dashboards custom). RGPD, propre, versionnee.

Base URL : https://webful.fr/api/v1 GET only JSON

Playground live

Voir chaque endpoint en action

Page de demonstration construite uniquement avec l'API publique, sur le compte demo@webful.fr (lecture seule). Chaque stat affiche le JSON brut et un exemple de code.

Ouvrir le playground

Introduction

L'API WEBFUL v1 permet de consulter toutes les donnees visibles dans votre dashboard, programmes dans vos scripts, workflows n8n/Make, ou interfaces internes. Lecture seule, couverture complete du dashboard (compte, sites, statistiques, performance, SEO, geolocalisation, funnels, webhooks, vue agency).

Aucune mutation (creation, modification, suppression) n'est exposee en v1 : l'API est lecture seule. Les operations d'administration restent dans le dashboard web.

Ne jamais embarquer une cle dans un frontend public. CORS est ouvert (Access-Control-Allow-Origin: *) pour permettre les dashboards internes et proxies serveur, mais toute cle exposee dans un JS accessible aux visiteurs serait immediatement captee. Appelez l'API depuis votre backend.

Conventions

  • Format : JSON (UTF-8). Seul GET est expose en v1.
  • Dates et horodatages : toutes les dates completes sont en UTC au format ISO 8601 (YYYY-MM-DDTHH:MM:SSZ, ex: 2026-04-22T15:30:00Z). Les dates seules (ex: current_period_end) restent au format YYYY-MM-DD.
  • Request ID : chaque reponse inclut un header X-Request-Id (et meta.request_id / error.request_id). Transmettez-le quand vous signalez un probleme.
  • Version : chaque reponse inclut X-API-Version: v1.
  • Pagination : page (>=1) et per_page (1-200). La reponse contient page, per_page, total et total_pages. Si page depasse total_pages, la liste retournee est vide.
  • Compression : gzip supporte en production. Envoyez Accept-Encoding: gzip pour l'activer. Les environnements de developpement locaux peuvent ne pas le negocier selon leur config Apache.
  • Parametres inconnus : les query parameters non documentes pour un endpoint sont silencieusement ignores. L'API ne renvoie BAD_REQUEST que pour les parametres documentes dont la valeur sort de l'enum ou des bornes.
  • Headers CORS exposes : les clients front peuvent lire X-Request-Id, X-API-Version, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset et X-Data-Freshness (listes dans Access-Control-Expose-Headers).
  • Fraicheur des donnees : pour les endpoints qui dependent d'un batch (PageSpeed, analyseur SEO), la reponse inclut le header X-Data-Freshness avec le timestamp ISO 8601 UTC de la donnee la plus recente disponible (ex: X-Data-Freshness: 2026-04-22T03:14:00Z). Header absent = donnees temps reel (insertion a chaque hit ou agregation a la requete). Concerne actuellement /sites/{id}/performance et /sites/{id}/seo.
  • Champs meta transverses : chaque reponse inclut meta.generated_at (ISO 8601 UTC), meta.api_version ("v1"), meta.request_id et meta.rate_limit. Les endpoints qui acceptent from/to reflechissent les bornes dans meta.period. Les endpoints qui acceptent un parametre filtre (granularity, view, category, by, type, limit, max_depth, country_code, etc.) le reflechissent dans meta (valeur effective appliquee).
  • Fraicheur des donnees : les endpoints dependants d'un batch (PageSpeed, SEO) exposent deux champs meta : meta.data_source (ex: "pagespeed_insights") et meta.refresh_cadence (enum ouverte, valeurs observees : "daily", "on_demand"). Le header X-Data-Freshness est en plus renvoye pour /performance (timestamp ISO du dernier batch).

Quickstart

  1. Connectez-vous au dashboard > Profil.
  2. Dans la section API Developpeurs, generez une cle API.
  3. Copiez la cle (affichee une seule fois, format wbf_live_...).
  4. Testez immediatement avec curl :
# Tester votre cle (doit retourner les infos du compte)
curl -H "Authorization: Bearer wbf_live_xxxxxxxxxxxxxxxxxxxx" \
     https://webful.fr/api/v1/account

Exemples dans d'autres langages : JavaScript / Python / PHP.

Exemples multi-langages

Les blocs ci-dessous montrent comment appeler GET /account dans chaque langage. Le pattern est identique pour tous les endpoints : Bearer token dans le header Authorization, JSON en retour. Substituez simplement le path et les query params.

Ne jamais embarquer la cle dans un frontend public. Appelez l'API depuis votre backend ou une fonction serverless (Cloudflare Worker, Vercel, Netlify).

cURL

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     -H "Accept: application/json" \
     "https://webful.fr/api/v1/account"

JavaScript (fetch, Node 18+ / navigateur)

const res = await fetch('https://webful.fr/api/v1/account', {
  headers: {
    'Authorization': `Bearer ${process.env.WEBFUL_KEY}`,
    'Accept':        'application/json',
  },
});

if (!res.ok) {
  const err = await res.json();
  throw new Error(`${err.error.code}: ${err.error.message}`);
}

const { data, meta } = await res.json();
console.log(data.plan, data.subscription_status);
// Rate limit : res.headers.get('X-RateLimit-Remaining')

Python (requests)

import os, requests

headers = {
    "Authorization": f"Bearer {os.environ['WEBFUL_KEY']}",
    "Accept":        "application/json",
}
res = requests.get("https://webful.fr/api/v1/account", headers=headers, timeout=10)

if res.status_code != 200:
    err = res.json()["error"]
    raise RuntimeError(f"{err['code']}: {err['message']} (request_id={err['request_id']})")

payload = res.json()
print(payload["data"]["plan"], payload["data"]["subscription_status"])
# Rate limit : res.headers["X-RateLimit-Remaining"]

PHP (cURL)

<?php
$ch = curl_init('https://webful.fr/api/v1/account');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('WEBFUL_KEY'),
        'Accept: application/json',
    ],
    CURLOPT_TIMEOUT        => 10,
]);
$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$json = json_decode($body, true);

if ($status !== 200) {
    throw new RuntimeException(
        $json['error']['code'] . ': ' . $json['error']['message']
    );
}

echo $json['data']['plan'], "\n";

Avec query parameters

Exemple : GET /sites/WBF-12345/pages?from=2026-04-01&to=2026-04-30&sort=visits&per_page=100

# JavaScript
const qs = new URLSearchParams({
  from: '2026-04-01', to: '2026-04-30',
  sort: 'visits', per_page: '100',
});
await fetch(`https://webful.fr/api/v1/sites/WBF-12345/pages?${qs}`, { headers });

# Python
params = {"from": "2026-04-01", "to": "2026-04-30",
          "sort": "visits", "per_page": 100}
requests.get(url, headers=headers, params=params)

Integrations no-code

  • n8n / Make / Zapier : utilisez un node "HTTP Request" GET, ajoutez le header Authorization: Bearer {{cle}}, mode d'auth "None" (l'auth est gerée par le header custom). Le JSON de reponse est directement parsé par le node suivant.
  • Postman / Insomnia / Bruno : importez le schema OpenAPI 3.0 pour generer automatiquement une collection avec les 18 endpoints et leurs parametres.
  • Google Sheets / Excel : via Apps Script (UrlFetchApp.fetch) ou Power Query. Stockez la cle dans les Script Properties (Sheets) ou un coffre-fort (Excel).

Authentification

Toutes les requetes authentifiees utilisent un Bearer token dans le header Authorization. La cle est liee a votre compte et donne acces a l'integralite des donnees du compte (tous les sites, tous les endpoints).

Authorization: Bearer wbf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/json

Pour revoquer une cle, regenerez-la ou utilisez le bouton Revoquer dans le dashboard. L'ancienne cle cesse immediatement de fonctionner.

Scopes granulaires (cle read-only, limitee a certains sites, multiples cles dev/staging/prod) : non disponibles en v1, prevus en v2.

Mode sandbox : non disponible en v1. Pour tester sans impacter vos donnees reelles, creez un compte dedie ou un site de test. Un vrai bac a sable (jeu de donnees fictif et cles wbf_test_) est prevu pour la v2.

Rate limiting

Le quota est compte par cle API (pas par compte, pas par IP) sur une fenetre glissante d'une heure. Chaque nouvelle requete "libere" la plus ancienne qui sort de la fenetre ; il n'y a pas de reset brutal.

PlanLimiteFenetre
Standard1 000 req1 heure glissante
Agence5 000 req1 heure glissante

Chaque reponse inclut les headers :

  • X-RateLimit-Limit : limite horaire applicable a la cle
  • X-RateLimit-Remaining : requetes restantes dans la fenetre courante
  • X-RateLimit-Reset : timestamp Unix (secondes) auquel le plus vieux hit sortira de la fenetre (indicatif, la fenetre est glissante)

Depassement : HTTP 429 avec code RATE_LIMITED et header Retry-After.

Erreurs

Les erreurs renvoient un JSON standardise. La liste des code est fermee (UPPER_SNAKE_CASE) : toute nouvelle valeur sera documentee avant introduction. Les champs code, message, documentation_url et request_id sont toujours presents. Le champ details est optionnel : il est fourni pour BAD_REQUEST (objet decrivant le parametre fautif) et parfois pour RATE_LIMITED (quota/fenetre). Ne dependez pas de sa presence systematique.

{ "error": { "code": "BAD_REQUEST", "message": "Invalid period. Allowed values: 1, 7, 30, 90 (days).", "documentation_url": "https://webful.fr/api-docs#bad-request", "request_id": "01KPV9CMHTV7W7915410A1CS91", "details": { "param": "period", "allowed": [1, 7, 30, 90] } } }
HTTPCodeSignificationCause probable
400BAD_REQUESTParametre invalide ou manquantValeur hors enum, format de date incorrect, per_page hors bornes.
401UNAUTHORIZEDAuthentification echoueeHeader Authorization manquant, cle invalide ou revoquee, format de cle incorrect.
403FORBIDDENAcces refuseEmail non verifie, compte suspendu, ressource appartenant a un autre compte.
404NOT_FOUNDRessource ou endpoint inconnuID de site invalide, URL d'endpoint inexistante.
429RATE_LIMITEDQuota horaire depasseVoir header Retry-After et X-RateLimit-Reset.
500INTERNAL_ERRORErreur serveurBug cote WEBFUL. Transmettez le request_id au support.

Enumerations

Les champs suivants sont des enumerations fermees : toute valeur retournee est garantie appartenir a la liste. Les ajouts se feront de facon retro-compatible (annoncee dans le changelog, jamais en silence).

ChampValeurs
account.planfree | solo | pro | agency | agency_plus | enterprise
account.subscription_statusactive | trialing | past_due | canceled | incomplete | inactive
site.planfree | premium
Le plan attache a un site (distinct du plan du compte). free = quota 1 000 visites/mois, premium = 100 000.
site.integration_typewordpress | html | shopify | prestashop | wix | squarespace
site.report_frequencynone | daily | weekly | monthly
site.report_modetraffic_only | traffic_plus_perf
status.activityok | warning | error
status.onlineok | warning | error | na
status.pluginok | warning | pending
status.perfok | warning | error | na
status.js_errorsok | error | na
alert.severityinfo | low | medium | high | critical
alert.code no_visit_ever, no_visit_recent, site_offline, plugin_pending, plugin_outdated, perf_warning, perf_critical, js_errors, traffic_drop
Selon le code, l'objet alert peut contenir des champs contextuels en plus de code et severity :
  • no_visit_recent : days_since_visit (int)
  • site_offline : http_status (int)
  • plugin_outdated : current (string), latest (string)
  • perf_warning / perf_critical : score (int 0-100)
  • js_errors : count (int)
  • traffic_drop : percent (float, variation negative en %)
metrics.trend_reliabilityfull | partial | none
stats.granularity (param)hour | day | month
pages.sort (param)visits | unique_visitors | avg_time
referrers.categorydirect | search | social | ai | referral | internal
Le parametre category accepte en plus la valeur all.
events.view (param)aggregate | stream
conversions.view (param)aggregate | stream
conversions.conversion_typetel_click | email_click | form_submit | page_visit | outbound_click | file_download
Le parametre type accepte en plus la valeur all.
performance.device (param)mobile | desktop | both
performance.report.statusexcellent | good | average | poor
seo.view (param)list | report
geolocation.by (param)country | region | city
funnel.step.typepageview | conversion | event
funnel.period (param)1 | 7 | 30 | 90 | 365 (jours, fenetre glissante)
webhook_delivery.statuspending | success | failed | retrying
Le parametre status accepte en plus la valeur all.
webhook.event_type traffic.alert | health.critical | health.recovered | conversion.new | error.js_recurring
threshold_value (integer, nullable) n'est utilise que pour error.js_recurring (seuil d'erreurs JS declenchant le webhook). null pour tous les autres types.

Endpoints disponibles

GET /api/v1/account

Demo

Informations du compte associe a la cle API (id, email, plan, statut abonnement).

Fraicheur : temps reel.

Exemple :

# Ping : verifie la cle et retourne les infos du compte
curl -H "Authorization: Bearer $WEBFUL_KEY" \
     https://webful.fr/api/v1/account

Reponse :

{
  "data": {
    "id": 42,
    "email": "alice@example.com",
    "plan": "pro",
    "subscription_status": "active",
    "current_period_end": "2026-05-15"    // date-only, null si abonnement sans echeance connue (free / canceled)
  },
  "meta": {
    "generated_at": "2026-04-22T15:30:00Z",
    "api_version": "v1",
    "request_id": "01KPV9CC3MA9641GSF1CW7SBKA",
    "rate_limit": { "limit": 1000, "remaining": 987, "reset": 1776888389 }
  }
}

GET /api/v1/sites

Demo

Liste paginee des sites appartenant au compte. Tri : created_at DESC (le plus recent en premier).

Fraicheur : temps reel.

Parametres :

  • page (int >= 1, defaut 1)
  • per_page (int, min 1, defaut 50, max 200)
curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites?per_page=20"

Reponse (extrait) :

{
  "data": [
    {
      "site_id": "WBF-12345",
      "name": "Mon site",
      "url": "https://example.com",
      "aliases": ["www.example.com"],
      "thumbnail_url": "https://webful.fr/uploads/thumbnails/WBF-12345.webp?v=1768234689",
      "plan": "premium",
      "integration_type": "wordpress",
      "plugin_version": "2.5.3",  // null si integration_type != "wordpress" ou plugin pas encore installe
      "is_plugin_outdated": false,    // bool ou null (voir note)
      "tracking_verified": true,     // opt-in manuel via le dashboard (voir note)
      "tracking_active": true,        // dérivé : first_visit_at != null
      "report_frequency": "weekly",
      "report_mode": "traffic_plus_perf",
      "created_at": "2026-01-15T09:22:10Z"
    }
  ],
  "pagination": {
    "page": 1, "per_page": 20, "total": 4, "total_pages": 1
  },
  "meta": {
    "latest_plugin_version": "2.5.3",   // reference pour is_plugin_outdated
    "request_id": "01KPV9CG8XD1G6D5EXDXMTKJJ2"
    /* generated_at, api_version, rate_limit */
  }
}

Champs derives : is_plugin_outdated = true si plugin_version < latest_plugin_version (semver), false sinon, null si la notion n'est pas applicable (integration_type != "wordpress", ou plugin pas encore installe). tracking_active = true si au moins une visite a ete enregistree (first_visit_at != null). Repond a la question "le tracker fonctionne-t-il ?". Distinct de tracking_verified, qui est un opt-in manuel : l'utilisateur a clique sur "Verifier mon tracking" dans le dashboard et WEBFUL a fetch le HTML pour confirmer la presence du snippet. Un site peut avoir tracking_active: true et tracking_verified: false (tracker en place mais verification manuelle jamais declenchee). C'est normal.

GET /api/v1/sites/{site_id}

Demo

Detail complet d'un site : metadonnees, plan, quota du mois en cours, dernier controle de sante, dernier snapshot performance et SEO.

Fraicheur : metadonnees et quota = temps reel ; health.* = dernier health-check (~30 min) ; performance et seo = dernier batch d'analyse (quotidien).

Parametres de chemin :

  • site_id (format WBF-XXXXX) : obligatoire. Doit appartenir au compte de la cle (sinon NOT_FOUND).
curl -H "Authorization: Bearer $WEBFUL_KEY" \
     https://webful.fr/api/v1/sites/WBF-12345

Reponse :

{
  "data": {
    "site_id": "WBF-12345",
    "name": "Mon site",
    "url": "https://example.com",
    "aliases": ["www.example.com"],
    "thumbnail_url": "https://webful.fr/uploads/thumbnails/WBF-12345.webp?v=1768234689",
    "plan": "premium",
    "integration_type": "wordpress",
    "plugin_version": "2.5.3",  // null si integration_type != "wordpress" ou plugin pas encore installe
    "is_plugin_outdated": false,    // bool ou null (voir note sous /sites)
    "tracking_verified": true,     // opt-in manuel via le dashboard
    "tracking_verified_at": "2025-10-21T13:56:21Z",
    "tracking_active": true,        // derive : first_visit_at != null (= "le tracker voit des visites")
    "report_frequency": "weekly",
    "report_mode": "traffic_plus_perf",
    "created_at": "2025-10-21T13:32:09Z",
    "first_visit_at": "2025-10-21T13:56:21Z",
    "last_visit_at": "2026-04-22T10:45:00Z",
    "quota": {
      "visits_this_month": 1846,
      "monthly_limit": 100000,       // null si plan sans limite connue
      "window": "calendar_month"
    },
    "health": {
      "is_online": true,
      "http_status": 200,
      "response_time_ms": 289,
      "checked_at": "2026-04-22T14:43:38Z"
    },                                      // null si aucun health-check
    "performance": {
      "score": 65,
      "device": "mobile",
      "analyzed_at": "2026-04-20T23:01:23Z"
    },                                      // null si jamais analyse
    "seo": {
      "score": 82,
      "analyzed_at": "2026-04-01T16:41:53Z"
    }                                       // null si jamais analyse
  },
  "meta": {
    "generated_at": "2026-04-22T19:50:17Z",
    "api_version": "v1",
    "request_id": "01KPVBWPXG0KG0FXV12D7Q5AAH",
    "site_id": "WBF-12345",
    "rate_limit": { "limit": 1000, "remaining": 992, "reset": 1776891106 }
  }
}

quota.monthly_limit est null si le plan du site n'a pas de limite documentee en v1 (ex: ancien plan custom). Dans ce cas, le champ visits_this_month reste valide comme simple compteur.

Les blocs health, performance et seo sont null tant qu'aucun run n'a eu lieu. Ne supposez jamais leur presence ; testez !== null avant d'acceder aux sous-champs.

GET /api/v1/sites/{site_id}/stats

Demo

Totaux et serie temporelle agregee pour un site sur une periode. Le filtre anti-bot (sessions avec ≥ 3 IPs distinctes) est applique automatiquement.

Fraicheur : temps reel (base tracker ecrite en direct).

Parametres :

  • from (YYYY-MM-DD, defaut J-30)
  • to (YYYY-MM-DD, defaut aujourd'hui). Fenetre max : 365 jours.
  • granularity (enum ferme hour | day | month, defaut day). Valeur hors liste = BAD_REQUEST.

La granularite hour fait apparaitre le bucket au format ISO 8601 UTC (2026-04-22T14:00:00Z). day utilise YYYY-MM-DD, month utilise YYYY-MM. Les buckets sans trafic sont absents de la serie (pas remplis a zero).

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/stats?from=2026-04-01&to=2026-04-22&granularity=day"

Reponse (extrait) :

{
  "data": {
    "totals": {
      "page_views": 2237,
      "unique_visitors": 1049,
      "sessions": 1484,
      "bounces": 1170,
      "bounce_rate": 78.8,            // % arrondi 1 decimale
      "avg_time_on_page": 1576,       // secondes, cappe a 1800 par visite (0 si aucune donnee)
      "conversions": 498             // table conversions, filtre anti-bot applique
    },
    "timeseries": [
      { "bucket": "2026-04-01", "page_views": 43, "unique_visitors": 22, "sessions": 25 },
      { "bucket": "2026-04-02", "page_views": 48, "unique_visitors": 29, "sessions": 34 }
      /* ... tri : bucket ASC */
    ]
  },
  "meta": {
    "generated_at": "2026-04-22T19:50:40Z",
    "api_version": "v1",
    "request_id": "01KPVBX9PY43Z49SJ7CKX7SSMF",
    "site_id": "WBF-12345",
    "period": { "from": "2026-04-01", "to": "2026-04-22" },
    "granularity": "day",
    "rate_limit": { "limit": 1000, "remaining": 991, "reset": 1776891106 }
  }
}

GET /api/v1/sites/{site_id}/pages

Demo

Top pages consultees sur la periode, agregees par url. Filtre anti-bot applique.

Fraicheur : temps reel.

Parametres :

  • from, to (YYYY-MM-DD ; defaut J-30 / aujourd'hui ; max 365 j)
  • page, per_page (pagination, max 200)
  • sort (enum ferme visits | unique_visitors | avg_time, defaut visits)

Tri : critere demande DESC, puis url ASC pour etre deterministe. avg_time ignore les visites a temps nul (champ vide ou 0) et est plafonne a 1800 s (30 min) par visite pour neutraliser les onglets oublies (standard GA/Matomo). Les durees reelles superieures sont donc comptees comme 1800.

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/pages?per_page=10&sort=visits"

Reponse (extrait) :

{
  "data": [
    {
      "url": "https://example.com/",
      "title": "Exemple - Accueil",    // peut etre null
      "visits": 554,
      "unique_visitors": 358,
      "avg_time": 1107                  // secondes, cappe a 1800 par visite (0 si inconnu)
    }
  ],
  "pagination": { "page": 1, "per_page": 10, "total": 87, "total_pages": 9 },
  "meta": {
    "request_id": "01KPVBX9R7RPNJ7E5YA4VC0V71",
    "site_id": "WBF-12345",
    "period": { "from": "2026-03-23", "to": "2026-04-22" },
    "sort": "visits",
    "rate_limit": { "limit": 1000, "remaining": 990, "reset": 1776891106 }
    /* generated_at, api_version egalement presents */
  }
}

GET /api/v1/sites/{site_id}/referrers

Demo

Sources de trafic agregees par host normalise (host en lowercase, sans www.) et categorisees. Les referrers vides ou invalides sont regroupes sous (direct).

Fraicheur : temps reel. Filtre anti-bot applique.

Parametres :

  • from, to (YYYY-MM-DD ; defaut J-30 / aujourd'hui ; max 365 j)
  • page, per_page (pagination, max 200)
  • category (enum ferme all | direct | search | social | ai | referral | internal, defaut all). Filtre sur la categorie retournee.

Categorisation : search (google, bing, yahoo, ddg, yandex, baidu, ecosia, qwant), social (facebook, x/twitter, linkedin, instagram, pinterest, reddit, tiktok, youtube, whatsapp), ai (chatgpt, claude, perplexity, gemini, copilot, you.com), internal (host du site lui-meme), direct (pas de referrer), referral (tout le reste). Tri : visits DESC, puis host ASC.

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/referrers?category=search"

Reponse (extrait) :

{
  "data": [
    { "host": "google.com", "category": "search", "visits": 429 },
    { "host": "bing.com",   "category": "search", "visits": 47 }
  ],
  "pagination": { "page": 1, "per_page": 50, "total": 5, "total_pages": 1 },
  "meta": {
    "request_id": "01KPVBZ962S72CWF1M6W99002W",
    "site_id": "WBF-12345",
    "period": { "from": "2026-03-23", "to": "2026-04-22" },
    "category": "search",
    "rate_limit": { "limit": 1000, "remaining": 989, "reset": 1776891106 }
  }
}

GET /api/v1/sites/{site_id}/events

Demo

Evenements custom trackes (tracker webful.track('nom', {...})). Deux vues : aggregee par event_name, ou flux chronologique brut.

Fraicheur : temps reel. Filtre anti-bot applique (meme logique que /stats, /pages, /conversions).

Parametres :

  • from, to (YYYY-MM-DD ; defaut J-30 / aujourd'hui ; max 365 j)
  • view (enum ferme aggregate | stream, defaut aggregate)
  • event_name (string libre, max 100 caracteres, match strict). Non une enum fermee : les integrateurs utilisent leurs propres noms. Compatible avec view=aggregate ET view=stream.
  • page, per_page (pagination, max 200) — pertinente uniquement en view=stream. En view=aggregate, le nombre de rows est borne par le nombre d'event_name distincts (rarement > 50) ; paginer n'a pas de sens.

Tri : aggregate = count DESC, puis event_name ASC. stream = timestamp DESC (le plus recent en premier).

# Vue aggregee : tous les event_name
curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/events"

# Vue aggregee, filtree sur un nom d'evenement (renvoie une seule row)
curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/events?event_name=signup"

# Flux chronologique, filtre sur un nom d'evenement
curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/events?view=stream&event_name=signup&per_page=50"

Reponse view=aggregate :

{
  "data": [
    {
      "event_name": "signup",
      "count": 142,
      "unique_sessions": 128,
      "first_seen": "2026-03-23T12:04:18Z",
      "last_seen": "2026-04-22T18:32:09Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 50, "total": 8, "total_pages": 1 },
  "meta": {
    "site_id": "WBF-12345",
    "view": "aggregate",
    "event_name": null,             // filtre actif si non null
    "period": { "from": "2026-03-23", "to": "2026-04-22" }
    /* request_id, generated_at, api_version, rate_limit */
  }
}

Reponse view=stream :

{
  "data": [
    {
      "id": 98412,
      "event_name": "signup",
      "data": { "plan": "pro", "source": "pricing" },  // JSON decode si possible, sinon string brute, sinon null
      "session_id": "WBF-1776784067916-v9p4anhjc",
      "timestamp": "2026-04-22T18:32:09Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 50, "total": 1284, "total_pages": 26 }
}

GET /api/v1/sites/{site_id}/conversions

Demo

Definition : une conversion est un evenement explicitement marque comme tel par le tracker (table conversions, distincte des evenements custom /events et des visites /stats). Les types sont une enum fermee de 6 valeurs.

Fraicheur : temps reel. Filtre anti-bot applique : les session_id dont les page_views presentent ≥ 3 ip_hash distincts sur la fenetre sont exclues, comme pour /stats, /pages, /referrers. Le ratio conversions/sessions est donc coherent entre tous les endpoints.

Parametres :

  • from, to (YYYY-MM-DD ; defaut J-30 / aujourd'hui ; max 365 j)
  • type (enum ferme all | tel_click | email_click | form_submit | page_visit | outbound_click | file_download, defaut all)
  • view (enum ferme aggregate | stream, defaut aggregate)
  • page, per_page (pagination, max 200 ; utilise seulement en view=stream)

Tri : aggregate = count DESC puis conversion_type ASC ; stream = timestamp DESC, puis id DESC.

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/conversions?view=aggregate"

Reponse view=aggregate :

{
  "data": [
    {
      "conversion_type": "page_visit",
      "count": 265,
      "unique_sessions": 159,
      "first_seen": "2026-03-23T14:06:20Z",
      "last_seen": "2026-04-21T13:10:02Z"
    },
    {
      "conversion_type": "form_submit",
      "count": 178,
      "unique_sessions": 72,
      "first_seen": "2026-03-25T23:24:05Z",
      "last_seen": "2026-04-19T09:15:04Z"
    }
  ],
  "meta": {
    "site_id": "WBF-12345",
    "period": { "from": "2026-03-23", "to": "2026-04-22" },
    "view": "aggregate",
    "type": "all",
    "total_conversions": 498,         // voir note sous l'exemple
    "rate_limit": { "limit": 1000, "remaining": 987, "reset": 1776891106 }
  }
}

meta.total_conversions : nombre total de conversions sur la periode (apres filtre anti-bot), respectant le parametre type. Avec type=all, c'est la somme de toutes les rows.count de la vue aggregate. Avec un filtre (type=form_submit), c'est le total pour ce type uniquement. Champ present dans les deux vues (aggregate et stream).

Reponse view=stream :

{
  "data": [
    {
      "id": 74218,
      "conversion_type": "form_submit",
      "label": "contact-form",          // libelle attache par le tracker, peut etre null
      "page_url": "https://example.com/contact",
      "referrer": "https://google.com/",      // peut etre null
      "device_type": "mobile",              // peut etre null, valeurs : mobile | desktop | tablet
      "session_id": "WBF-1776784067916-v9p4anhjc",
      "visitor_id": "WBF-1776784068139-o9dkc0wfy",  // format identique a session_id (cf note ci-dessous)
      "timestamp": "2026-04-19T09:15:04Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 50, "total": 498, "total_pages": 10 }
}

Le champ visitor_id est un identifiant pseudonyme non-trackant : il ne permet pas de re-identifier un individu hors du site concerne. Ne le stockez pas comme cle utilisateur longue duree.

Note contrat : WEBFUL ne fait pas de re-identification cross-session. En pratique, visitor_id coincide donc actuellement avec session_id (1 visite = 1 identifiant). Les deux champs restent distincts pour ne pas casser l'integration si un mode opt-in visiteur persistant est ajoute plus tard. Ne presumez pas d'unicite visitor_id vs session_id dans vos calculs.

GET /api/v1/agency/overview

Demo

Snapshot de la vue agency : pour chaque site, voyants de sante, metriques (visites, tendance, scores perf/SEO), alertes et score de sante global.

Fraicheur : visites en temps reel ; perf_score / seo_score = dernier batch d'analyse (quotidien) ; voyant online = check sante toutes les 30 minutes.

Parametres :

  • period (enum ferme 1 | 7 | 30 | 90, defaut 30) : fenetre de comparaison en jours. Valeur hors liste = BAD_REQUEST.

Pourquoi une enum fermee ici, alors que /sites/{id}/stats accepte from/to libres ? /agency/overview est une vue snapshot synthetique (voyants, alertes, score de sante global), pas un endpoint de stats libres. Les 4 valeurs 1 | 7 | 30 | 90 correspondent aux fenetres pour lesquelles la fiabilite de trend_percent est calibree (trend_reliability) et dont le calcul "periode vs periode precedente" reste performant meme avec beaucoup de sites. Pour des plages libres, utilisez /sites/{id}/stats (jusqu'a 365 jours via from/to).

trend_percent

Compare les visites de la periode courante a la periode precedente de meme duree (ex: period=7 compare les 7 derniers jours aux 7 jours d'avant). Valeurs : -100 a +inf, arrondi a 1 decimale. 0 si aucun trafic sur la periode precedente, 100 si nouveau trafic. Le champ trend_reliability (full | partial | none) indique si la comparaison est statistiquement significative.

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/agency/overview?period=7"

Reponse (extrait) :

{
  "data": {
    "summary": {
      "total_sites": 4,
      "total_visits": 12840,
      "period_days": 7,
      "avg_trend": 12.3,
      "avg_perf_score": 87,
      "total_alerts": 2
    },
    "sites": [
      {
        "site_id": "WBF-12345",
        "name": "Mon site",
        "url": "https://example.com",
        "thumbnail_url": "https://webful.fr/uploads/thumbnails/WBF-12345.webp?v=1768234689",
        "integration_type": "wordpress",
        "plugin_version": "2.5.3",  // null si integration_type != "wordpress" ou plugin pas encore installe
        "is_plugin_outdated": false,    // bool ou null (voir note sous /sites)
        "tracking_active": true,        // derive : au moins une visite enregistree
        "report_frequency": "weekly",
        "report_mode": "traffic_plus_perf",
        "created_at": "2026-01-15T09:22:10Z",
        "status": {
          "activity": "ok",    // voir section Enumerations
          "online": "ok",
          "plugin": "ok",
          "perf": "warning",
          "js_errors": "ok"
        },
        "metrics": {
          "visits": 3210,
          "visits_previous": 2787,
          "trend_percent": 15.2,
          "trend_reliability": "full",
          "perf_score": 64,
          "seo_score": 82,
          "last_visit": "2026-04-22T10:45:00Z",
          "days_since_visit": 0,
          "js_errors_24h": 0
        },
        "health_score": 80,
        "alerts": [                  // voir section Enumerations : code + severity + champs contextuels variables
          { "code": "perf_warning",  "severity": "low",    "score": 64 },
          { "code": "traffic_drop",  "severity": "medium", "percent": -39.3 }
        ]
      }
    ]
  },
  "meta": {
    "generated_at": "2026-04-22T15:30:00Z",
    "api_version": "v1",
    "request_id": "01KPV9CMHTV7W7915410A1CS91",
    "period": { "days": 7, "label": "7d" },
    "latest_plugin_version": "2.5.3",
    "rate_limit": { "limit": 5000, "remaining": 4987, "reset": 1776888389 }
  }
}

meta.period reprend le parametre effectivement utilise (apres validation) ; meta.latest_plugin_version = derniere version stable du plugin WordPress WEBFUL, sert de reference pour le voyant status.plugin.

Les sites renvoyes incluent les memes champs que /api/v1/sites (url, thumbnail_url, integration_type, etc.) pour eviter un second appel N+1.

status.plugin pour les sites non-WordPress (integration HTML, Shopify, etc.) : ok des que du trafic est detecte, pending tant qu'aucune visite n'a ete enregistree. Pas de na ni warning dans ce cas.

GET /api/v1/account/usage

Demo

Consommation du quota mensuel du compte : vues utilisees ce mois, plafond du plan, pourcentage, breakdown par site.

Fraicheur : temps reel (mise a jour a chaque hit trackable). Fenetre : mois calendaire UTC (YYYY-MM). Remise a zero au 1er de chaque mois a 00:00 UTC.

Parametres :

Aucun.

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/account/usage"

Reponse :

{
  "data": {
    "period": "2026-04",
    "views_used": 12840,
    "views_limit": 100000,        // null si plan illimite
    "views_remaining": 87160,    // null si illimite
    "percentage": 12.84,         // null si illimite
    "unlimited": false,
    "per_site": [
      { "site_id": "WBF-12345", "views": 8210 },
      { "site_id": "WBF-67890", "views": 4630 }
    ],
    "resets_at": "2026-05-01T00:00:00Z",
    "api_requests_last_24h": 272
  },
  "meta": {
    "plan": "agency",
    "rate_limit": { "limit": 5000, "remaining": 4999, "reset": 1776944400 }
  }
}

per_site[].views est garanti coherent avec quota.visits_this_month renvoye par /sites/{id} : meme source, meme fenetre (mois calendaire UTC), meme filtre anti-bot.
api_requests_last_24h : nombre de requetes API faites par ce compte sur les 24 dernieres heures glissantes, toutes cles API cumulees, tous status HTTP confondus (2xx, 4xx, 5xx). A ne pas confondre avec meta.rate_limit qui est scope par cle sur une fenetre d'1h.

GET /api/v1/sites/{site_id}/performance

Demo

Scores PageSpeed Insights du dernier batch + historique jusqu'a 90 jours. Source : table performance_analyses, alimentee par un CRON quotidien (~03:00 UTC).

Fraicheur : batch quotidien. Un site pas encore analyse renvoie latest = null (ou latest.{mobile|desktop} = null en mode device=both), jamais des 0. Header X-Data-Freshness renvoye avec l'horodatage ISO du dernier batch disponible.

Parametres :

  • device (enum ferme mobile | desktop | both, defaut mobile)
  • history_days (int 0-90, defaut 30) : 0 = pas d'historique retourne (seulement latest)

Tri historique : analyzed_at ASC (du plus ancien au plus recent).

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/performance?device=mobile&history_days=30"

Reponse :

{
  "data": {
    "latest": {
      "analyzed_at": "2026-04-22T03:12:44Z",
      "device_type": "mobile",
      "scores": {
        "performance": 92,    // 0-100 ou null
        "accessibility": 89,
        "seo": 95,
        "best_practices": 100
      },
      "metrics": {
        "lcp_seconds": 1.06,
        "fid_ms": 16,
        "cls": 0,
        "fcp_seconds": 1.06,
        "ttfb_seconds": 0.2,
        "speed_index_seconds": 1.83,
        "tbt_ms": 0
      },
      "report": {
        "status": "excellent",     // voir Enumerations : excellent | good | average | poor
        "message": "Bravo ! Votre site est parfaitement optimise."
      }
    },
    "history": [
      { "analyzed_at": "2026-03-23T03:12:44Z", "device_type": "mobile",
        "performance": 88, "accessibility": 89, "seo": 95, "best_practices": 100,
        "lcp_seconds": 1.4, "cls": 0, "tbt_ms": 20 }
    ]
  },
  "meta": {
    "site_id": "WBF-12345",
    "device": "mobile",
    "history_days": 30,
    "data_source": "pagespeed_insights",
    "refresh_cadence": "daily"
  }
}

Reponse (device=both, extrait) :

{
  "data": {
    "latest": {
      "mobile":  { "analyzed_at": "2026-04-22T03:12:44Z",
                  "scores": { "performance": 92, ... },
                  "metrics": { ... }, "report": { ... } },
      "desktop": { "analyzed_at": "2026-04-22T03:14:02Z",
                  "scores": { "performance": 98, ... },
                  "metrics": { ... }, "report": { ... } }
      // Si l'un des deux devices n'a pas encore ete analyse : null a la place de l'objet.
    },
    "history": [
      { "analyzed_at": "2026-04-21T03:11:02Z", "device_type": "mobile",  "performance": 90, ... },
      { "analyzed_at": "2026-04-21T03:11:48Z", "device_type": "desktop", "performance": 97, ... }
      // Les entrees mobile et desktop sont melees, triees par analyzed_at ASC.
      // Filtrer cote client sur device_type si un seul graphe par device est souhaite.
    ]
  },
  "meta": { "site_id": "WBF-12345", "device": "both", "history_days": 30,
            "data_source": "pagespeed_insights", "refresh_cadence": "daily" }
}

En mode device=both, data.latest devient un objet {"mobile": ..., "desktop": ...}. Si un seul device a ete analyse, la cle manquante vaut null (et non un objet vide). L'historique retourne les analyses des deux devices meles, chacune portant son propre device_type.

GET /api/v1/sites/{site_id}/seo

Demo

Analyses SEO stockees : liste des URLs analysees (view=list) ou rapport detaille pour une URL precise (view=report).

Fraicheur : batch a la demande (declenche manuellement depuis l'outil SEO du dashboard). Les URLs jamais analysees n'apparaissent pas.

Parametres :

  • view (enum ferme list | report, defaut list)
  • url (string, obligatoire si view=report) : URL exacte analysee (max 500 caracteres)
  • page, per_page (pagination, max 200 ; utilise seulement en view=list)

Tri (view=list) : analyzed_at DESC, puis id DESC.

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/seo?view=list"

Reponse view=list :

{
  "data": [
    {
      "url": "https://example.com/",
      "score": 88,
      "score_previous": 82,                    // nullable : null a la 1re analyse
      "score_delta": 6,                         // nullable : null a la 1re analyse
      "analyzed_at": "2026-04-15T10:22:00Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 50, "total": 12, "total_pages": 1 },
  "meta": {
    "site_id": "WBF-12345",
    "view": "list",
    "data_source": "webful_seo_analyzer",
    "refresh_cadence": "on_demand"     // relance manuelle depuis le dashboard
  }
}

Reponse view=report :

{
  "data": {
    "url": "https://example.com/",
    "score": 88,
    "score_previous": 82,                      // nullable
    "score_delta": 6,                           // nullable
    "analyzed_at": "2026-04-15T10:22:00Z",
    "results": [
      { "rule_id": "title_exists",    "passed": true,  "message": "Balise title presente",
        "value": "Accueil - Example" },     // string
      { "rule_id": "h1_count",        "passed": true,  "message": "1 balise H1",
        "value": 1 },                       // integer
      { "rule_id": "text_html_ratio", "passed": true,  "message": "Ratio texte/HTML correct",
        "value": 0.27 },                    // float
      { "rule_id": "mobile_viewport", "passed": true,  "message": "Viewport mobile present",
        "value": true },                    // boolean
      { "rule_id": "broken_links",    "passed": false, "message": "3 lien(s) casse(s)",
        "value": {                          // object (structure propre au rule_id)
          "total_links": 42,
          "broken_count": 3,
          "broken_links": ["/old-page", "https://dead.example.com"]
        } },
      { "rule_id": "meta_description", "passed": false, "message": "Meta description absente",
        "value": null }                     // null
    ]
  },
  "meta": {
    "site_id": "WBF-12345",
    "view": "report",
    "data_source": "webful_seo_analyzer",
    "refresh_cadence": "on_demand"
  }
}

Les rule_id sont stables entre versions et servent de cles pour mapper vers vos propres libelles. Liste exhaustive via le dashboard (section SEO).

results[].value est un type variable dependant du rule_id et du fait que la regle passe ou echoue :

  • string : regles qui renvoient un contenu textuel (title_exists, h1_unique, meta_description).
  • integer : regles de comptage (title_length, word_count, external_scripts, og_tags).
  • float : ratios et scores (text_html_ratio, page_speed).
  • boolean : presence/absence d'un element HTML (heading_structure, https, mobile_viewport, canonical).
  • object : details d'echec pour certaines regles, structure propre a chaque rule_id. Exemples observes : broken_links{total_links, broken_count, broken_links: [url, ...]} ; images_alt{total, without_alt, missing_alt_images: [url, ...]}. Une meme regle peut renvoyer un integer quand elle passe et un object quand elle echoue (ex: broken_links).
  • null : regle qui a echoue avant d'avoir pu mesurer.

Coder defensivement cote client : tester typeof value (ou Array.isArray / is_array) avant d'appeler .toString(), une methode de formatage ou un template. La liste ci-dessus est indicative : de nouvelles regles peuvent s'ajouter en version mineure, et le type de value pour une regle donnee peut etre etendu (ex: integer → object) en version mineure aussi, tant que le type reste parseable de maniere defensive.

Chaque regle appartient a une categorie (critical | important | optimization) qui determine sa penalite ; la categorie n'est pas retournee cote API pour l'instant.

GET /api/v1/sites/{site_id}/geolocation

Demo

Repartition geographique du trafic : pays, regions ou villes des visiteurs.

Fraicheur : temps reel. La resolution d'une IP est differee de quelques minutes (job async), donc les visites tres recentes peuvent encore etre non-geolocalisees et exclues du total. Filtre anti-bot applique.

Parametres :

  • from, to (YYYY-MM-DD ; defaut J-30 / aujourd'hui ; max 365 j)
  • by (enum ferme country | region | city, defaut country)
  • country_code (ISO 3166-1 alpha-2, ex: FR, US) : filtre pour obtenir les regions/villes d'un pays donne
  • page, per_page (max 200)

Tri : views DESC, puis nom ASC (determinisme).

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/geolocation?by=city&country_code=FR"

Reponse (by=city, country_code=FR) :

{
  "data": [
    {
      "country_code": "FR",
      "country_name": "France",
      "region_name": "Paris Department",
      "city_name": "Paris",
      "views": 1448,
      "unique_visitors": 61,
      "sessions": 248,
      "percentage": 70.77
    }
  ],
  "meta": {
    "site_id": "WBF-12345",
    "period": { "from": "2026-03-23", "to": "2026-04-22" },
    "by": "city",
    "country_code": "FR",                  // string | null : filtre effectivement applique
    "total_views": 2046
  },
  "pagination": { "page": 1, "per_page": 50, "total": 4, "total_pages": 1 }
}

En by=country, les champs renvoyes sont country_code, country_name + metriques. En by=region, s'ajoute region_name. En by=city, s'ajoutent region_name et city_name. percentage = part du total (apres filtres) attribuee a ce groupe.
meta.country_code reflete le filtre applique (null si non specifie).

GET /api/v1/sites/{site_id}/user-flows

Demo

Parcours utilisateurs : pages d'entree, pages de sortie, top sequences de pages visitees, et stats d'engagement.

Fraicheur : temps reel (agregation directe sur page_views). Filtre anti-bot applique.

Parametres :

  • from, to (YYYY-MM-DD ; defaut J-7 / aujourd'hui ; max 365 j)
  • limit (int 1-50, defaut 10) : taille des top listes (entry_pages, exit_pages, top_paths)
  • max_depth (int 2-10, defaut 10) : profondeur maximale d'un parcours retenu dans top_paths

Tri : entry_pages / exit_pages : count DESC, puis url ASC. top_paths : occurrences DESC (paths vus moins de 2 fois exclus).

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/user-flows?limit=5"

Reponse :

{
  "data": {
    "stats": {
      "total_sessions": 101,
      "avg_pages_per_session": 5.09,
      "bounce_rate": 65.35,
      "bounces": 66
    },
    "entry_pages": [
      { "url": "https://example.com/", "count": 20, "percentage": 19.8 }
    ],
    "exit_pages": [
      { "url": "https://example.com/contact", "count": 14, "percentage": 13.86 }
    ],
    "top_paths": [
      {
        "pages": ["https://example.com/", "https://example.com/pricing"],
        "depth": 2,
        "occurrences": 12,
        "percentage": 11.88
      }
    ]
  },
  "meta": {
    "site_id": "WBF-12345",
    "period": { "from": "2026-04-15", "to": "2026-04-22" },
    "limit": 5,                          // valeur effective du parametre limit
    "max_depth": 10                       // valeur effective du parametre max_depth
  }
}

GET /api/v1/sites/{site_id}/funnels

Demo

Liste des entonnoirs de conversion configures pour le site. Pour les donnees de conversion d'un funnel precis, utiliser l'endpoint de detail ci-dessous.

Fraicheur : temps reel (configuration en BDD).

Parametres :

  • page, per_page (max 200)

Tri : created_at DESC.

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/funnels"

Reponse :

{
  "data": [
    {
      "id": 1,
      "name": "Lead direct",
      "steps": [
        { "order": 1, "type": "pageview",   "value": "https://example.com",         "label": "Accueil" },
        { "order": 2, "type": "pageview",   "value": "https://example.com/contact", "label": "Contact" },
        { "order": 3, "type": "conversion", "value": "form_submit",               "label": "Formulaire envoye" }
      ],
      "created_at": "2026-03-29T18:58:29Z",
      "updated_at": "2026-03-29T18:58:29Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 50, "total": 1, "total_pages": 1 }
}

GET /api/v1/sites/{site_id}/funnels/{funnel_id}

Donnees de conversion d'un funnel : visiteurs uniques a chaque etape, dropoff, taux de conversion.

Fraicheur : temps reel. Fenetre : glissante depuis maintenant.

Parametres :

  • period (enum ferme 1 | 7 | 30 | 90 | 365 jours, defaut 30)
curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/funnels/1?period=30"

Reponse :

{
  "data": {
    "id": 1,
    "name": "Lead direct",
    "total_entered": 400,
    "total_converted": 47,
    "conversion_rate": 11.75,
    "steps": [
      { "order": 1, "type": "pageview",   "value": "https://example.com",
        "label": "Accueil", "visitors": 400, "dropoff": 0,
        "rate_from_previous": 100, "rate_from_first": 100 },
      { "order": 2, "type": "pageview",   "value": "https://example.com/contact",
        "label": "Contact", "visitors": 72, "dropoff": 328,
        "rate_from_previous": 18, "rate_from_first": 18 },
      { "order": 3, "type": "conversion", "value": "form_submit",
        "label": "Formulaire envoye", "visitors": 47, "dropoff": 25,
        "rate_from_previous": 65.28, "rate_from_first": 11.75 }
    ]
  },
  "meta": { "site_id": "WBF-12345", "funnel_id": 1, "period_days": 30, "window": "rolling" }
}

step.type = enum fermee pageview | conversion | event (voir Enumerations). Le calcul dedoublonne par ip_hash : un meme visiteur qui complete le funnel dans plusieurs sessions ne compte qu'une fois.
step.label : peut etre null dans /funnels (liste) si le libelle n'a pas ete saisi en configuration. Dans /funnels/{id} (detail), label prend value comme fallback pour garantir un affichage non-vide cote client.
meta.period_days (int) reflete le parametre period applique ; meta.window vaut "rolling" (fenetre glissante depuis maintenant).
Un funnel sans steps (configuration incomplete) renvoie steps: [] avec total_entered: 0, total_converted: 0, conversion_rate: 0.

GET /api/v1/sites/{site_id}/webhooks

Demo

Liste des webhooks sortants configures pour ce site (URL cible, evenements souscrits, etat de la derniere livraison).

Fraicheur : temps reel. Securite : le secret HMAC du webhook n'est jamais expose par l'API (consultation/regeneration via le dashboard uniquement).

Parametres :

  • page, per_page (max 200)

Tri : created_at DESC.

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/webhooks"

Reponse :

{
  "data": [
    {
      "id": 17,
      "name": "Slack - Conversions",
      "url": "https://hooks.slack.com/services/...",
      "is_active": true,
      "events": [
        { "event_type": "conversion.new",        "threshold_value": null },
        { "event_type": "error.js_recurring",    "threshold_value": 5 }
      ],
      "last_delivery": {                      // null si aucune livraison encore
        "event_type": "conversion.new",
        "status": "success",                 // voir Enumerations
        "response_code": 200,                  // nullable (echec reseau)
        "created_at": "2026-04-22T14:30:00Z",
        "delivered_at": "2026-04-22T14:30:00Z"
      },
      "created_at": "2026-03-12T10:00:00Z",
      "updated_at": "2026-04-01T08:15:00Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 50, "total": 1, "total_pages": 1 }
}

Types d'evenements (enum fermee, voir Enumerations) : traffic.alert, health.critical, health.recovered, conversion.new, error.js_recurring.
threshold_value (integer, nullable) : utilise uniquement pour error.js_recurring (nombre minimum d'erreurs JS recurrentes declenchant le webhook). null pour tous les autres types d'evenements.
last_delivery est null si le webhook n'a jamais ete declenche. response_code est null en cas d'echec reseau (timeout, DNS, TLS) avant reception d'une reponse HTTP.


GET /api/v1/sites/{site_id}/webhooks/{webhook_id}/deliveries

Historique des livraisons (payload envoye, code HTTP recu, status). Le cycle de retry est : tentative initiale, puis jusqu'a 5 retries avec backoff exponentiel avant abandon.

Fraicheur : temps reel.

Parametres :

  • status (enum ferme all | pending | success | failed | retrying, defaut all)
  • page, per_page (max 200)

Tri : created_at DESC, puis id DESC.

curl -H "Authorization: Bearer $WEBFUL_KEY" \
     "https://webful.fr/api/v1/sites/WBF-12345/webhooks/17/deliveries?status=failed"

Reponse :

{
  "data": [
    {
      "id": 4521,
      "delivery_id": "d-3c7b1ae2-4f80-4e8a-9c3e-2b1d8e0a1234",
      "event_type": "conversion.new",
      "status": "failed",
      "attempt": 5,
      "response_code": 500,                      // nullable (null en echec reseau)
      "response_body": "Internal Server Error",    // nullable, tronque a 2000 caracteres
      "payload": { "conversion_type": "form_submit", "site_id": "WBF-12345" },
      "created_at": "2026-04-22T10:00:00Z",
      "delivered_at": null,                      // null tant que non delivre avec succes
      "next_retry_at": null                       // null si succes ou abandon
    }
  ],
  "pagination": { "page": 1, "per_page": 50, "total": 12, "total_pages": 1 },
  "meta": {
    "site_id": "WBF-12345",
    "webhook": { "id": 17, "name": "Slack - Conversions", "url": "https://...", "is_active": true },
    "status": "failed"
  }
}

delivery_id : identifiant stable de la livraison, format d-<uuid-v4>. Inclus en header X-Webful-Delivery-Id du payload envoye au client, utilisable pour correler une livraison recue cote receveur avec cette ligne d'historique.
attempt : numero de tentative (1 = initiale, 2-5 = retries avec backoff exponentiel).
next_retry_at est null si la livraison a abouti (status=success) ou si tous les retries sont epuises (status=failed avec attempt=5).

Schema OpenAPI 3.0

Un schema OpenAPI 3.0.3 decrit formellement tous les endpoints, parametres, enums et formes de reponse. Il est servi en YAML sur :

https://webful.fr/api/v1/openapi.yaml

Utilisations typiques :

  • Generation de SDK : openapi-generator-cli generate -i https://webful.fr/api/v1/openapi.yaml -g python -o ./webful-client (remplacez python par typescript-fetch, go, php, etc.)
  • Import dans Postman / Insomnia / Bruno : collection complete generee automatiquement.
  • Import dans n8n / Make : les connecteurs HTTP recents acceptent l'URL du schema pour pre-remplir les appels.
  • Rendu interactif : collez l'URL dans editor.swagger.io ou redocly.github.io/redoc pour une doc navigable avec "Try it out".

CORS ouvert sur le schema (header Access-Control-Allow-Origin: *) : les outils cote navigateur peuvent le recuperer directement. Pas d'authentification requise.

Le schema est maintenu a la main en parallele de cette page. Source de verite : cette documentation ; toute divergence observee est un bug a signaler.

Versioning & breaking changes

  • Versionning dans l'URL : les endpoints sont prefixes par /api/v1/. Une v2 vivra en parallele de la v1, pas en remplacement.
  • Changements retro-compatibles (nouveau champ optionnel, nouvelle valeur ajoutee a une enum, nouvel endpoint) : livres sans preavis, documentes dans le changelog. Un integrateur defensif doit tolerer les champs inconnus.
  • Breaking changes (suppression d'un champ, changement de type, restriction d'une enum) : uniquement via une nouvelle version majeure. La version precedente reste maintenue 6 mois minimum apres annonce.
  • Canal d'annonce des deprecations :
    1. Email aux proprietaires de cles API actives (adresse du compte)
    2. Changelog public sur cette page (section "Changelog" en bas)
    3. Header Deprecation (RFC 8594) et Sunset retournes sur les endpoints/champs concernes a partir de l'annonce
  • Header X-API-Version : renvoye a chaque reponse (ex: v1). Utile pour diagnostiquer en cas de proxy qui reecrit les URLs.

Changelog

v1.0 — 23 avril 2026

lancement

Version initiale de l'API publique WEBFUL. 18 endpoints en lecture seule couvrant l'integralite du dashboard : compte, sites, statistiques (stats, pages, referrers, events, conversions), performance, SEO, geolocalisation, parcours utilisateurs, funnels, webhooks, vue agency multi-sites.

Authentification Bearer par cle API compte-level, rate limiting par cle (1 000 req/h standard, 5 000 req/h agence), dates ISO 8601 UTC, request_id trace sur chaque reponse, enumerations fermees documentees, pagination standardisee, filtres anti-bot appliques sur les endpoints de trafic, X-Data-Freshness sur les endpoints batch (performance, SEO).

Outillage : schema OpenAPI 3.0.3 servi en ligne (CORS ouvert), exemples d'integration en cURL, JavaScript, Python, PHP et no-code (n8n, Make, Postman, Google Sheets).

Les evolutions futures (ajouts retro-compatibles et breaking changes via v2) seront journalisees ici et annoncees par email aux proprietaires de cles actives.

Roadmap

La v1 expose en lecture l'integralite du dashboard. Les chantiers suivants sont a l'etude pour les prochaines versions :

  • Rendu interactif de la doc (Swagger UI / Redoc) alimenté par le schema OpenAPI deja en ligne.
  • CORS configurable par compte : whitelist d'origines depuis le dashboard pour les usages frontaux maitrises (au-dela du * par defaut).
  • Scopes granulaires (cles read-only, restreintes a un sous-ensemble de sites, cles dev/staging/prod) — necessite une refonte de la table cles, prevu en v2.
  • Mode sandbox avec jeu de donnees fictif et cles wbf_test_ — v2.
  • Mutations (POST/PUT/DELETE) pour gestion programmatique des sites, funnels, webhooks — v2.
  • SDKs officiels (JavaScript, Python, PHP) — v2.

Un retour a nous partager ? Ecrivez-nous depuis la page contact.