API WEBFUL v1
REST API to read your WEBFUL analytics data from any app, script or automation (n8n, Make, custom dashboards). GDPR-compliant, clean, versioned.
https://webful.com/api/v1
GET only
JSON
Technical content in English. Detailed endpoint descriptions, code samples and the playground are available in English only. This matches the industry standard (Stripe, Resend, OpenAI...) and avoids the risk of documentation drifting between two languages.
Live playground
See each endpoint in action
Demo page built entirely with the public API, on the demo@webful.fr account (read-only). Each stat shows the raw JSON and a code sample.
Introduction
The WEBFUL v1 API lets you read all the data visible in your dashboard, from your scripts, n8n/Make workflows, or internal interfaces. Read-only, with full coverage of the dashboard (account, sites, statistics, performance, SEO, geolocation, funnels, webhooks, agency view).
No mutations (create, update, delete) are exposed in v1: the API is read-only. Administrative operations remain in the web dashboard.
Never embed a key in a public frontend.
CORS is open (Access-Control-Allow-Origin: *) to allow internal dashboards
and server-side proxies, but any key exposed in JS accessible to visitors would be
captured immediately. Call the API from your backend.
Conventions
- Format: JSON (UTF-8). Only
GETis exposed in v1. - Dates and timestamps: all full timestamps are in
UTC, ISO 8601 format
(
YYYY-MM-DDTHH:MM:SSZ, e.g.2026-04-22T15:30:00Z). Date-only fields (e.g.current_period_end) stay inYYYY-MM-DDformat. - Request ID: every response includes an
X-Request-Idheader (andmeta.request_id/error.request_id). Pass it along when you report an issue. - Version: every response includes
X-API-Version: v1. - Pagination:
page(>=1) andper_page(1-200). The response includespage,per_page,totalandtotal_pages. Ifpageexceedstotal_pages, the returned list is empty. - Compression: gzip supported in production. Send
Accept-Encoding: gzipto enable it. Local development environments may not negotiate it depending on their Apache config. - Unknown parameters: query parameters not documented for an endpoint
are silently ignored. The API only returns
BAD_REQUESTfor documented parameters whose value is outside the enum or allowed bounds. - Exposed CORS headers: frontend clients can read
X-Request-Id,X-API-Version,X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-ResetandX-Data-Freshness(listed inAccess-Control-Expose-Headers). - Data freshness: for endpoints that depend on a batch
(PageSpeed, SEO analyzer), the response includes the
X-Data-Freshnessheader with the ISO 8601 UTC timestamp of the most recent data available (e.g.X-Data-Freshness: 2026-04-22T03:14:00Z). Header absent = real-time data (inserted on each hit or aggregated at request time). Currently applies to/sites/{id}/performanceand/sites/{id}/seo. - Cross-cutting
metafields: every response includesmeta.generated_at(ISO 8601 UTC),meta.api_version("v1"),meta.request_idandmeta.rate_limit. Endpoints that acceptfrom/toreflect the bounds inmeta.period. Endpoints that accept a filter parameter (granularity,view,category,by,type,limit,max_depth,country_code, etc.) echo it inmeta(effective value applied). - Data freshness: batch-dependent endpoints (PageSpeed, SEO)
expose two
metafields:meta.data_source(e.g."pagespeed_insights") andmeta.refresh_cadence(open enum, observed values:"daily","on_demand"). TheX-Data-Freshnessheader is additionally returned for/performance(ISO timestamp of the last batch).
Quickstart
- Sign in to the dashboard > Profile.
- In the Developer API section, generate an API key.
- Copy the key (shown only once, format
wbf_live_...). - Test it right away with
curl:
# Test your key (should return your account info) curl -H "Authorization: Bearer wbf_live_xxxxxxxxxxxxxxxxxxxx" \ https://webful.com/api/v1/account
Examples in other languages: JavaScript / Python / PHP.
Multi-language examples
The blocks below show how to call GET /account
in each language. The pattern is identical for every endpoint: Bearer token in the
Authorization header, JSON in response. Just substitute the path and query params.
Never embed the key in a public frontend. Call the API from your backend or a serverless function (Cloudflare Worker, Vercel, Netlify).
cURL
curl -H "Authorization: Bearer $WEBFUL_KEY" \ -H "Accept: application/json" \ "https://webful.com/api/v1/account"
JavaScript (fetch, Node 18+ / browser)
const res = await fetch('https://webful.com/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.com/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.com/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";
With query parameters
Example: 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.com/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)
No-code integrations
- n8n / Make / Zapier: use a GET "HTTP Request" node,
add the header
Authorization: Bearer {{key}}, auth mode "None" (auth is handled by the custom header). The response JSON is parsed directly by the next node. - Postman / Insomnia / Bruno: import the OpenAPI 3.0 schema to automatically generate a collection with all 18 endpoints and their parameters.
- Google Sheets / Excel: via Apps Script (
UrlFetchApp.fetch) or Power Query. Store the key in Script Properties (Sheets) or a secure vault (Excel).
Authentication
All authenticated requests use a Bearer token in the
Authorization header. The key is tied to your account and grants
access to all account data (every site, every endpoint).
Authorization: Bearer wbf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx Accept: application/json
To revoke a key, regenerate it or use the Revoke button in the dashboard. The old key stops working immediately.
Granular scopes (read-only key, restricted to specific sites, multiple dev/staging/prod keys): not available in v1, planned for v2.
Sandbox mode: not available in v1.
To test without affecting your real data, create a dedicated account or a test site.
A true sandbox (fixture dataset and wbf_test_ keys)
is planned for v2.
Rate limiting
The quota is counted per API key (not per account, not per IP) over a sliding one-hour window. Each new request "frees up" the oldest one leaving the window; there is no hard reset.
| Plan | Limit | Window |
|---|---|---|
| Standard | 1,000 req | 1 hour sliding |
| Agency | 5,000 req | 1 hour sliding |
Every response includes the headers:
X-RateLimit-Limit: hourly limit applicable to the keyX-RateLimit-Remaining: requests remaining in the current windowX-RateLimit-Reset: Unix timestamp (seconds) at which the oldest hit will leave the window (indicative, the window is sliding)
When exceeded: HTTP 429 with code RATE_LIMITED and a Retry-After header.
Errors
Errors return a standardized JSON payload. The code list is closed
(UPPER_SNAKE_CASE): any new value will be documented before being introduced.
The code, message, documentation_url and request_id
fields are always present. The details field is optional:
it is provided for BAD_REQUEST (object describing the offending parameter) and sometimes for
RATE_LIMITED (quota/window). Do not rely on it being systematically present.
| HTTP | Code | Meaning | Likely cause |
|---|---|---|---|
| 400 | BAD_REQUEST | Invalid or missing parameter | Value outside enum, incorrect date format, per_page out of bounds. |
| 401 | UNAUTHORIZED | Authentication failed | Missing Authorization header, invalid or revoked key, incorrect key format. |
| 403 | FORBIDDEN | Access denied | Unverified email, suspended account, resource belonging to another account. |
| 404 | NOT_FOUND | Unknown resource or endpoint | Invalid site ID, non-existent endpoint URL. |
| 429 | RATE_LIMITED | Hourly quota exceeded | See Retry-After and X-RateLimit-Reset headers. |
| 500 | INTERNAL_ERROR | Server error | Bug on the WEBFUL side. Share the request_id with support. |
Enumerations
The following fields are closed enums: any returned value is guaranteed to be in the list. Additions will be backwards-compatible (announced in the changelog, never silently).
| Field | Values |
|---|---|
| account.plan | free | solo | pro | agency | agency_plus | enterprise |
| account.subscription_status | active | trialing | past_due | canceled | incomplete | inactive |
| site.plan | free | premiumThe plan attached to a site (distinct from the account plan). free = 1,000 visits/month quota, premium = 100,000. |
| site.integration_type | wordpress | html | shopify | prestashop | wix | squarespace |
| site.report_frequency | none | daily | weekly | monthly |
| site.report_mode | traffic_only | traffic_plus_perf |
| status.activity | ok | warning | error |
| status.online | ok | warning | error | na |
| status.plugin | ok | warning | pending |
| status.perf | ok | warning | error | na |
| status.js_errors | ok | error | na |
| alert.severity | info | 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
Depending on the code, the
alert object may contain contextual fields in addition to code and severity:
|
| metrics.trend_reliability | full | partial | none |
| stats.granularity (param) | hour | day | month |
| pages.sort (param) | visits | unique_visitors | avg_time |
| referrers.category | direct | search | social | ai | referral | internalThe category parameter additionally accepts the value all. |
| events.view (param) | aggregate | stream |
| conversions.view (param) | aggregate | stream |
| conversions.conversion_type | tel_click | email_click | form_submit | page_visit | outbound_click | file_downloadThe type parameter additionally accepts the value all. |
| performance.device (param) | mobile | desktop | both |
| performance.report.status | excellent | good | average | poor |
| seo.view (param) | list | report |
| geolocation.by (param) | country | region | city |
| funnel.step.type | pageview | conversion | event |
| funnel.period (param) | 1 | 7 | 30 | 90 | 365 (days, sliding window) |
| webhook_delivery.status | pending | success | failed | retryingThe status parameter additionally accepts the value all. |
| webhook.event_type |
traffic.alert | health.critical | health.recovered | conversion.new | error.js_recurring
threshold_value (integer, nullable) is only used for error.js_recurring
(JS error threshold that triggers the webhook). null for every other type.
|
Available endpoints
GET /api/v1/account
Demo Information about the account associated with the API key (id, email, plan, subscription status).
Freshness: real-time.
Example:
# Ping: verifies the key and returns the account info curl -H "Authorization: Bearer $WEBFUL_KEY" \ https://webful.com/api/v1/account
Response:
{
"data": {
"id": 42,
"email": "alice@example.com",
"plan": "pro",
"subscription_status": "active",
"current_period_end": "2026-05-15" // date-only, null if subscription has no known end date (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 Paginated list of sites belonging to the account.
Sort: created_at DESC (newest first).
Freshness: real-time.
Parameters:
page(int >= 1, default1)per_page(int, min1, default50, max200)
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites?per_page=20"
Response (excerpt):
{
"data": [
{
"site_id": "WBF-12345",
"name": "My 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 if integration_type != "wordpress" or plugin not yet installed
"is_plugin_outdated": false, // bool or null (see note)
"tracking_verified": true, // manual opt-in via the dashboard (see note)
"tracking_active": true, // derived: 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 for is_plugin_outdated
"request_id": "01KPV9CG8XD1G6D5EXDXMTKJJ2"
/* generated_at, api_version, rate_limit */
}
}
Derived fields:
is_plugin_outdated = true if plugin_version < latest_plugin_version
(semver), false otherwise, null if not applicable
(integration_type != "wordpress", or plugin not yet installed).
tracking_active = true if at least one visit has been recorded
(first_visit_at != null). Answers the question "is the tracker working?".
Distinct from tracking_verified, which is a manual opt-in: the user
clicked "Verify my tracking" in the dashboard and WEBFUL fetched the HTML to
confirm the snippet is present. A site can have tracking_active: true
and tracking_verified: false (tracker in place but manual verification
never triggered). This is normal.
GET /api/v1/sites/{site_id}
Demo Full details for a site: metadata, plan, current-month quota, last health check, latest performance and SEO snapshots.
Freshness: metadata and quota = real-time;
health.* = latest health check (~30 min);
performance and seo = latest analysis batch (daily).
Path parameters:
site_id(formatWBF-XXXXX): required. Must belong to the account of the key (otherwiseNOT_FOUND).
curl -H "Authorization: Bearer $WEBFUL_KEY" \
https://webful.com/api/v1/sites/WBF-12345
Response:
{
"data": {
"site_id": "WBF-12345",
"name": "My 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 if integration_type != "wordpress" or plugin not yet installed
"is_plugin_outdated": false, // bool or null (see note under /sites)
"tracking_verified": true, // manual opt-in via the dashboard
"tracking_verified_at": "2025-10-21T13:56:21Z",
"tracking_active": true, // derived: first_visit_at != null (= "the tracker is seeing visits")
"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 if plan has no known limit
"window": "calendar_month"
},
"health": {
"is_online": true,
"http_status": 200,
"response_time_ms": 289,
"checked_at": "2026-04-22T14:43:38Z"
}, // null if no health check yet
"performance": {
"score": 65,
"device": "mobile",
"analyzed_at": "2026-04-20T23:01:23Z"
}, // null if never analyzed
"seo": {
"score": 82,
"analyzed_at": "2026-04-01T16:41:53Z"
} // null if never analyzed
},
"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 is null if the site's
plan has no documented limit in v1 (e.g. legacy custom plan). In that case, the
visits_this_month field remains valid as a simple counter.
The health, performance and
seo blocks are null as long as no run has occurred. Never assume they are present;
test !== null before accessing subfields.
GET /api/v1/sites/{site_id}/stats
Demo Totals and aggregated time series for a site over a period. The anti-bot filter (sessions with ≥ 3 distinct IPs) is applied automatically.
Freshness: real-time (tracker database written live).
Parameters:
from(YYYY-MM-DD, defaultD-30)to(YYYY-MM-DD, default today). Max window: 365 days.granularity(closed enumhour | day | month, defaultday). Value outside the list =BAD_REQUEST.
The hour granularity returns buckets
in ISO 8601 UTC format (2026-04-22T14:00:00Z). day uses
YYYY-MM-DD, month uses YYYY-MM. Buckets with no traffic
are absent from the series (not filled with zeros).
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/stats?from=2026-04-01&to=2026-04-22&granularity=day"
Response (excerpt):
{
"data": {
"totals": {
"page_views": 2237,
"unique_visitors": 1049,
"sessions": 1484,
"bounces": 1170,
"bounce_rate": 78.8, // % rounded to 1 decimal
"avg_time_on_page": 1576, // seconds, capped at 1800 per visit (0 if no data)
"conversions": 498 // conversions table, anti-bot filter applied
},
"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 }
/* ... sort: 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 viewed over the period, aggregated by url.
Anti-bot filter applied.
Freshness: real-time.
Parameters:
from,to(YYYY-MM-DD; default D-30 / today; max 365 days)page,per_page(pagination, max 200)sort(closed enumvisits | unique_visitors | avg_time, defaultvisits)
Sort: requested criterion DESC, then url ASC
for determinism. avg_time ignores visits with zero time (empty or 0)
and is capped at 1800 s (30 min) per visit to neutralize forgotten tabs
(GA/Matomo standard). Actual durations above that are therefore counted as 1800.
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/pages?per_page=10&sort=visits"
Response (excerpt):
{
"data": [
{
"url": "https://example.com/",
"title": "Example - Home", // may be null
"visits": 554,
"unique_visitors": 358,
"avg_time": 1107 // seconds, capped at 1800 per visit (0 if unknown)
}
],
"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 also present */
}
}
GET /api/v1/sites/{site_id}/referrers
Demo Traffic sources aggregated by normalized host
(host lowercased, without www.) and categorized.
Empty or invalid referrers are grouped under (direct).
Freshness: real-time. Anti-bot filter applied.
Parameters:
from,to(YYYY-MM-DD; default D-30 / today; max 365 days)page,per_page(pagination, max 200)category(closed enumall | direct | search | social | ai | referral | internal, defaultall). Filters on the returned category.
Categorization:
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 of the site itself),
direct (no referrer),
referral (anything else). Sort: visits DESC, then host ASC.
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/referrers?category=search"
Response (excerpt):
{
"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 Custom events tracked (tracker webful.track('name', {...})).
Two views: aggregated by event_name, or raw chronological stream.
Freshness: real-time.
Anti-bot filter applied (same logic as /stats, /pages, /conversions).
Parameters:
from,to(YYYY-MM-DD; default D-30 / today; max 365 days)view(closed enumaggregate | stream, defaultaggregate)event_name(free string, max 100 characters, strict match). Not a closed enum: integrators use their own names. Works with bothview=aggregateANDview=stream.page,per_page(pagination, max 200) — only relevant inview=stream. Inview=aggregate, the row count is bounded by the number of distinctevent_namevalues (rarely > 50); paginating serves no purpose.
Sort:
aggregate = count DESC, then event_name ASC.
stream = timestamp DESC (newest first).
# Aggregate view: all event_names curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/events" # Aggregate view, filtered on one event name (returns a single row) curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/events?event_name=signup" # Chronological stream, filtered on one event name curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/events?view=stream&event_name=signup&per_page=50"
Response 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, // filter active if not null
"period": { "from": "2026-03-23", "to": "2026-04-22" }
/* request_id, generated_at, api_version, rate_limit */
}
}
Response view=stream:
{
"data": [
{
"id": 98412,
"event_name": "signup",
"data": { "plan": "pro", "source": "pricing" }, // decoded JSON if possible, otherwise raw string, otherwise 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: a conversion is an event
explicitly marked as such by the tracker (conversions table, distinct from
custom events /events and from visits /stats). Types
are a closed enum of 6 values.
Freshness: real-time.
Anti-bot filter applied: session_id values whose page_views
show ≥ 3 distinct ip_hash in the window are excluded, just like for
/stats, /pages, /referrers. The
conversions/sessions ratio is therefore consistent across all endpoints.
Parameters:
from,to(YYYY-MM-DD; default D-30 / today; max 365 days)type(closed enumall | tel_click | email_click | form_submit | page_visit | outbound_click | file_download, defaultall)view(closed enumaggregate | stream, defaultaggregate)page,per_page(pagination, max 200; only used inview=stream)
Sort:
aggregate = count DESC then conversion_type ASC;
stream = timestamp DESC, then id DESC.
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/conversions?view=aggregate"
Response 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, // see note under the example
"rate_limit": { "limit": 1000, "remaining": 987, "reset": 1776891106 }
}
}
meta.total_conversions: total number
of conversions in the period (after anti-bot filter), honoring the
type parameter. With type=all, it's the sum of all
rows.count in the aggregate view. With a filter (type=form_submit),
it's the total for that type only. Field present in both views
(aggregate and stream).
Response view=stream:
{
"data": [
{
"id": 74218,
"conversion_type": "form_submit",
"label": "contact-form", // label attached by the tracker, may be null
"page_url": "https://example.com/contact",
"referrer": "https://google.com/", // may be null
"device_type": "mobile", // may be null, values: mobile | desktop | tablet
"session_id": "WBF-1776784067916-v9p4anhjc",
"visitor_id": "WBF-1776784068139-o9dkc0wfy", // same format as session_id (see note below)
"timestamp": "2026-04-19T09:15:04Z"
}
],
"pagination": { "page": 1, "per_page": 50, "total": 498, "total_pages": 10 }
}
The visitor_id field is a pseudonymous,
non-tracking identifier: it does not allow re-identifying an individual outside the site in question. Do not store it
as a long-term user key.
Contract note: WEBFUL does
not perform cross-session re-identification. In practice, visitor_id
therefore currently matches session_id (1 visit = 1 identifier). The two
fields remain distinct to avoid breaking integrations if a persistent visitor opt-in mode
is added later. Do not assume visitor_id vs session_id uniqueness
in your calculations.
GET /api/v1/agency/overview
Demo Agency view snapshot: for each site, health indicators, metrics (visits, trend, perf/SEO scores), alerts and overall health score.
Freshness: visits real-time;
perf_score / seo_score = latest analysis batch (daily);
online indicator = health check every 30 minutes.
Parameters:
period(closed enum1 | 7 | 30 | 90, default30): comparison window in days. Value outside the list =BAD_REQUEST.
Why a closed enum here,
when /sites/{id}/stats accepts free from/to?
/agency/overview is a synthetic snapshot view (indicators, alerts, overall
health score), not a free-form stats endpoint. The 4 values 1 | 7 | 30 | 90
correspond to the windows for which trend_percent reliability is
calibrated (trend_reliability) and whose "period vs previous period" computation
stays performant even with many sites. For free-form ranges, use
/sites/{id}/stats (up to 365 days via from/to).
trend_percent
Compares visits in the current period to the previous period
of the same length (e.g. period=7 compares the last 7 days to the 7 days before).
Values: -100 to +inf, rounded to 1 decimal. 0 if no traffic
in the previous period, 100 for brand new traffic.
The trend_reliability field (full | partial | none)
indicates whether the comparison is statistically significant.
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/agency/overview?period=7"
Response (excerpt):
{
"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": "My 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 if integration_type != "wordpress" or plugin not yet installed
"is_plugin_outdated": false, // bool or null (see note under /sites)
"tracking_active": true, // derived: at least one visit recorded
"report_frequency": "weekly",
"report_mode": "traffic_plus_perf",
"created_at": "2026-01-15T09:22:10Z",
"status": {
"activity": "ok", // see Enumerations section
"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": [ // see Enumerations section: code + severity + variable contextual fields
{ "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 echoes the parameter actually used
(after validation); meta.latest_plugin_version = latest stable version of the
WordPress WEBFUL plugin, used as a reference for the status.plugin indicator.
The sites returned include the same fields as
/api/v1/sites (url, thumbnail_url, integration_type, etc.)
to avoid an N+1 follow-up call.
status.plugin for non-WordPress sites
(HTML integration, Shopify, etc.): ok as soon as traffic is detected, pending as long as
no visit has been recorded. No na or warning in this case.
GET /api/v1/account/usage
Demo Account monthly quota consumption: views used this month, plan cap, percentage, per-site breakdown.
Freshness: real-time (updated on every trackable hit).
Window: UTC calendar month (YYYY-MM). Resets on the 1st of each month at 00:00 UTC.
Parameters:
None.
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/account/usage"
Response:
{
"data": {
"period": "2026-04",
"views_used": 12840,
"views_limit": 100000, // null if unlimited plan
"views_remaining": 87160, // null if unlimited
"percentage": 12.84, // null if unlimited
"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 is guaranteed
consistent with quota.visits_this_month returned by /sites/{id}: same source,
same window (UTC calendar month), same anti-bot filter.
api_requests_last_24h: number of API requests made by
this account over the last 24 rolling hours, across all API keys,
all HTTP statuses combined (2xx, 4xx, 5xx). Not to be confused with
meta.rate_limit, which is scoped per key over a 1-hour window.
GET /api/v1/sites/{site_id}/performance
Demo PageSpeed Insights scores from the latest batch + up to 90 days of history.
Source: performance_analyses table, populated by a daily CRON (~03:00 UTC).
Freshness: daily batch.
A site that has not yet been analyzed returns latest = null (or latest.{mobile|desktop} = null
in device=both mode), never zeros. X-Data-Freshness header returned with
the ISO timestamp of the latest available batch.
Parameters:
device(closed enummobile | desktop | both, defaultmobile)history_days(int 0-90, default 30): 0 = no history returned (onlylatest)
History sort: analyzed_at ASC (oldest to newest).
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/performance?device=mobile&history_days=30"
Response:
{
"data": {
"latest": {
"analyzed_at": "2026-04-22T03:12:44Z",
"device_type": "mobile",
"scores": {
"performance": 92, // 0-100 or 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", // see Enumerations: excellent | good | average | poor
"message": "Great job! Your site is perfectly optimized."
}
},
"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"
}
}
Response (device=both, excerpt):
{
"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": { ... } }
// If one of the two devices has not yet been analyzed: null instead of the object.
},
"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, ... }
// Mobile and desktop entries are interleaved, sorted by analyzed_at ASC.
// Filter client-side on device_type if a separate chart per device is desired.
]
},
"meta": { "site_id": "WBF-12345", "device": "both", "history_days": 30,
"data_source": "pagespeed_insights", "refresh_cadence": "daily" }
}
In device=both mode, data.latest
becomes an object {"mobile": ..., "desktop": ...}. If only one device has been analyzed,
the missing key is null (not an empty object). The history returns the analyses
for both devices interleaved, each carrying its own device_type.
GET /api/v1/sites/{site_id}/seo
Demo Stored SEO analyses: list of analyzed URLs (view=list)
or detailed report for a specific URL (view=report).
Freshness: on-demand batch (triggered manually from the SEO tool in the dashboard). URLs never analyzed do not appear.
Parameters:
view(closed enumlist | report, defaultlist)url(string, required ifview=report): exact analyzed URL (max 500 characters)page,per_page(pagination, max 200; only used inview=list)
Sort (view=list): analyzed_at DESC, then id DESC.
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/seo?view=list"
Response view=list:
{
"data": [
{
"url": "https://example.com/",
"score": 88,
"score_previous": 82, // nullable: null on first analysis
"score_delta": 6, // nullable: null on first analysis
"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" // manual rerun from the dashboard
}
}
Response 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": "Title tag present",
"value": "Home - Example" }, // string
{ "rule_id": "h1_count", "passed": true, "message": "1 H1 tag",
"value": 1 }, // integer
{ "rule_id": "text_html_ratio", "passed": true, "message": "Text/HTML ratio looks good",
"value": 0.27 }, // float
{ "rule_id": "mobile_viewport", "passed": true, "message": "Mobile viewport present",
"value": true }, // boolean
{ "rule_id": "broken_links", "passed": false, "message": "3 broken link(s)",
"value": { // object (structure specific to 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 missing",
"value": null } // null
]
},
"meta": {
"site_id": "WBF-12345",
"view": "report",
"data_source": "webful_seo_analyzer",
"refresh_cadence": "on_demand"
}
}
The rule_id values are stable across versions and serve
as keys to map to your own labels. Full list via the dashboard (SEO section).
results[].value has a variable type
depending on the rule_id and whether the rule passes or fails:
- string: rules that return textual content
(
title_exists,h1_unique,meta_description). - integer: counting rules
(
title_length,word_count,external_scripts,og_tags). - float: ratios and scores
(
text_html_ratio,page_speed). - boolean: presence/absence of an HTML element
(
heading_structure,https,mobile_viewport,canonical). - object: failure details for some rules, structure specific to each
rule_id. Observed examples:broken_links->{total_links, broken_count, broken_links: [url, ...]};images_alt->{total, without_alt, missing_alt_images: [url, ...]}. The same rule can return anintegerwhen it passes and anobjectwhen it fails (e.g.broken_links). - null: rule that failed before it could measure.
Code defensively client-side:
test typeof value (or Array.isArray / is_array) before
calling .toString(), a formatting method, or a template. The list above
is indicative: new rules can be added in a minor version, and the type of
value for a given rule can be extended (e.g. integer -> object) in a minor
version too, as long as the type stays defensively parseable.
Each rule belongs to a category (critical | important | optimization)
that determines its penalty; the category is not returned via the API for now.
GET /api/v1/sites/{site_id}/geolocation
Demo Geographic distribution of traffic: countries, regions, or cities of visitors.
Freshness: real-time. IP resolution is delayed by a few minutes (async job), so very recent visits may still be ungeolocated and excluded from the total. Anti-bot filter applied.
Parameters:
from,to(YYYY-MM-DD; default D-30 / today; max 365 days)by(closed enumcountry | region | city, defaultcountry)country_code(ISO 3166-1 alpha-2, e.g.FR,US): filter to obtain regions/cities of a given countrypage,per_page(max 200)
Sort: views DESC, then name ASC (determinism).
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/geolocation?by=city&country_code=FR"
Response (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: filter actually applied
"total_views": 2046
},
"pagination": { "page": 1, "per_page": 50, "total": 4, "total_pages": 1 }
}
With by=country, the returned fields are
country_code, country_name + metrics. With by=region,
region_name is added. With by=city, region_name
and city_name are added. percentage = share of the total (after filters) attributed to this group.
meta.country_code reflects the applied filter (null if not specified).
GET /api/v1/sites/{site_id}/user-flows
Demo User flows: entry pages, exit pages, top sequences of pages visited, and engagement stats.
Freshness: real-time (direct aggregation on page_views).
Anti-bot filter applied.
Parameters:
from,to(YYYY-MM-DD; default D-7 / today; max 365 days)limit(int 1-50, default 10): size of the top lists (entry_pages,exit_pages,top_paths)max_depth(int 2-10, default 10): maximum depth of a path kept intop_paths
Sort:
entry_pages / exit_pages: count DESC, then url ASC.
top_paths: occurrences DESC (paths seen fewer than 2 times are excluded).
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/user-flows?limit=5"
Response:
{
"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, // effective value of the limit parameter
"max_depth": 10 // effective value of the max_depth parameter
}
}
GET /api/v1/sites/{site_id}/funnels
Demo List of conversion funnels configured for the site. For conversion data of a specific funnel, use the detail endpoint below.
Freshness: real-time (configuration in DB).
Parameters:
page,per_page(max 200)
Sort: created_at DESC.
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/funnels"
Response:
{
"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": "Form submitted" }
],
"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}
Conversion data for a funnel: unique visitors at each step, dropoff, conversion rate.
Freshness: real-time. Window: sliding from now.
Parameters:
period(closed enum1 | 7 | 30 | 90 | 365days, default30)
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/funnels/1?period=30"
Response:
{
"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": "Form submitted", "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 = closed enum
pageview | conversion | event (see Enumerations). The calculation deduplicates by
ip_hash: a single visitor who completes the funnel across multiple sessions is counted once.
step.label: may be null in /funnels (list) if the label
was not entered in configuration. In /funnels/{id} (detail), label
falls back to value to guarantee a non-empty display on the client side.
meta.period_days (int) reflects the applied period parameter;
meta.window is "rolling" (sliding window from now).
A funnel with no steps (incomplete configuration) returns steps: []
with total_entered: 0, total_converted: 0, conversion_rate: 0.
GET /api/v1/sites/{site_id}/webhooks
Demo List of outgoing webhooks configured for this site (target URL, subscribed events, status of the last delivery).
Freshness: real-time.
Security: the webhook's HMAC secret is never
exposed by the API (view/regenerate via the dashboard only).
Parameters:
page,per_page(max 200)
Sort: created_at DESC.
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/webhooks"
Response:
{
"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 if no delivery yet
"event_type": "conversion.new",
"status": "success", // see Enumerations
"response_code": 200, // nullable (network failure)
"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 }
}
Event types (closed enum,
see Enumerations): traffic.alert, health.critical,
health.recovered, conversion.new, error.js_recurring.
threshold_value (integer, nullable): used only for
error.js_recurring (minimum number of recurring JS errors that triggers the webhook).
null for all other event types.
last_delivery is null if the webhook has never been triggered.
response_code is null in case of network failure (timeout, DNS, TLS)
before receiving an HTTP response.
GET /api/v1/sites/{site_id}/webhooks/{webhook_id}/deliveries
Delivery history (payload sent, HTTP response code, status). The retry cycle is: initial attempt, then up to 5 retries with exponential backoff before giving up.
Freshness: real-time.
Parameters:
status(closed enumall | pending | success | failed | retrying, defaultall)page,per_page(max 200)
Sort: created_at DESC, then id DESC.
curl -H "Authorization: Bearer $WEBFUL_KEY" \ "https://webful.com/api/v1/sites/WBF-12345/webhooks/17/deliveries?status=failed"
Response:
{
"data": [
{
"id": 4521,
"delivery_id": "d-3c7b1ae2-4f80-4e8a-9c3e-2b1d8e0a1234",
"event_type": "conversion.new",
"status": "failed",
"attempt": 5,
"response_code": 500, // nullable (null on network failure)
"response_body": "Internal Server Error", // nullable, truncated to 2000 characters
"payload": { "conversion_type": "form_submit", "site_id": "WBF-12345" },
"created_at": "2026-04-22T10:00:00Z",
"delivered_at": null, // null until successfully delivered
"next_retry_at": null // null on success or give up
}
],
"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: stable delivery identifier,
format d-<uuid-v4>. Included as the X-Webful-Delivery-Id header of the payload sent
to the client, usable to correlate a delivery received on the receiver side with this history row.
attempt: attempt number (1 = initial, 2-5 = retries with exponential backoff).
next_retry_at is null if the delivery succeeded (status=success)
or if all retries are exhausted (status=failed with attempt=5).
OpenAPI 3.0 schema
An OpenAPI 3.0.3 schema formally describes all endpoints, parameters, enums and response shapes. It is served in YAML at:
https://webful.com/api/v1/openapi.yaml
Typical uses:
- SDK generation:
openapi-generator-cli generate -i https://webful.com/api/v1/openapi.yaml -g python -o ./webful-client(replacepythonwithtypescript-fetch,go,php, etc.) - Import into Postman / Insomnia / Bruno: full collection generated automatically.
- Import into n8n / Make: recent HTTP connectors accept the schema URL to pre-fill calls.
- Interactive rendering: paste the URL into editor.swagger.io or redocly.github.io/redoc for navigable docs with "Try it out".
CORS open on the schema (header
Access-Control-Allow-Origin: *): browser-side tools can fetch it directly.
No authentication required.
The schema is hand-maintained alongside this page. Source of truth: this documentation; any observed divergence is a bug to report.
Versioning & breaking changes
- Versioning in the URL: endpoints are prefixed with
/api/v1/. A v2 will live alongside v1, not as a replacement. - Backwards-compatible changes (new optional field, new value added to an enum, new endpoint): shipped without notice, documented in the changelog. A defensive integrator must tolerate unknown fields.
- Breaking changes (field removal, type change, enum restriction): only via a new major version. The previous version remains maintained for 6 months minimum after announcement.
- Deprecation announcement channel:
- Email to owners of active API keys (account address)
- Public changelog on this page ("Changelog" section at the bottom)
- Headers
Deprecation(RFC 8594) andSunsetreturned on affected endpoints/fields starting from the announcement
- Header
X-API-Version: returned on every response (e.g.v1). Useful for diagnostics in case of a proxy that rewrites URLs.
Changelog
v1.0 — April 23, 2026
launchInitial version of the WEBFUL public API. 18 read-only endpoints covering the entire dashboard: account, sites, statistics (stats, pages, referrers, events, conversions), performance, SEO, geolocation, user journeys, funnels, webhooks, multi-site agency view.
Bearer authentication with account-level API key,
per-key rate limiting (1,000 req/h standard, 5,000 req/h agency), ISO 8601 UTC dates,
request_id traced on every response, documented closed enumerations,
standardized pagination, anti-bot filters applied on traffic endpoints,
X-Data-Freshness on batch endpoints (performance, SEO).
Tooling: OpenAPI 3.0.3 schema served online (CORS open), integration examples in cURL, JavaScript, Python, PHP and no-code (n8n, Make, Postman, Google Sheets).
Future changes (backwards-compatible additions and breaking changes via v2) will be logged here and announced by email to owners of active keys.
Roadmap
v1 exposes the entire dashboard in read mode. The following areas are under consideration for upcoming versions:
- Interactive rendering of the docs (Swagger UI / Redoc) powered by the OpenAPI schema already online.
- Per-account configurable CORS: origin whitelist from the dashboard
for controlled front-end use cases (beyond the default
*). - Granular scopes (read-only keys, restricted to a subset of sites, dev/staging/prod keys) — requires a redesign of the keys table, planned for v2.
- Sandbox mode with fictitious dataset and
wbf_test_keys — v2. - Mutations (POST/PUT/DELETE) for programmatic management of sites, funnels, webhooks — v2.
- Official SDKs (JavaScript, Python, PHP) — v2.
Feedback to share? Write to us from the contact page.