Developers
REST API
Programmatic access to your workspace: contacts, submissions, events, test stats, workflow triggers and webhooks. Prefer an AI assistant doing the work? See the MCP documentation.
Authentication
Create an API key in the dashboard under API & webhooks (workspace admin role required; available on Pro and Scale plans). The token — fs_live_… — is shown once at creation; only its hash is stored. Send it as a Bearer token on every request:
curl https://app.funnelsplit.com/v1/contacts \
-H "Authorization: Bearer fs_live_YOUR_TOKEN"
- Base URL:
https://app.funnelsplit.com— all endpoints below are relative to it. - All requests and responses are JSON (
content-type: application/json). - Keys are scoped (below) and can be revoked instantly from the dashboard.
Scopes
Each key carries an explicit set of scopes. A request needing a scope the key lacks gets 403 insufficient_scope.
Data API scopes
| Scope | Unlocks |
|---|---|
contacts:rw | Read, create and update contacts; read activity timelines |
submissions:r | Read form submissions |
events:w | Ingest custom conversion events |
tests:r | Read A/B test stats |
workflows:trigger | Manually fire active workflows |
AI / content-control scopes (MCP)
These scopes power the MCP endpoint, which lets AI assistants build and manage content:
| Scope | Unlocks (via MCP tools) |
|---|---|
pages:rw | Create, edit, version, publish and unpublish pages |
forms:rw | Create, edit and publish forms; read them with warnings |
tests:rw | Full A/B test control: variants, goals, start/pause/complete, split links |
workflows:rw | Create, edit, activate and pause workflows; read run logs |
analytics:r | Read traffic rollups and funnel stats |
templates:rw | List and clone page/form/workflow templates |
media:r | List media-library assets and their URLs |
Errors & rate limits
- Error responses:
{ "error": "<code>", "message"?: "...", "details"?: [...] }. 401 unauthorized— missing, malformed, unknown or revoked token (indistinguishable by design).403 insufficient_scope— key lacks the required scope;403 plan_limit_exceeded— plan no longer includes the API.400 invalid_body— validation failed;detailslists the issues.404 not_found— no such resource in your workspace.- Rate limit: 120 requests/minute per key (shared between REST and MCP). Over-limit requests get
429 rate_limitedwith aRetry-Afterheader (seconds).
Contacts
GET /v1/contacts contacts:rw
List/search contacts. Query params: search (matches email/fields), limit (default 50, max 200), offset.
curl "https://app.funnelsplit.com/v1/contacts?search=jane&limit=10" \
-H "Authorization: Bearer fs_live_…"
// 200
{ "contacts": [ { "id": "…", "email": "jane@acme.com", "fields": { "plan": "trial" }, "createdAt": "…" } ] }
POST /v1/contacts contacts:rw
Upsert a contact by email. Creating a new contact fires contact_created workflow triggers. Returns 201 when created, 200 when updated.
curl -X POST https://app.funnelsplit.com/v1/contacts \
-H "Authorization: Bearer fs_live_…" -H "content-type: application/json" \
-d '{ "email": "jane@acme.com", "fields": { "company": "Acme", "plan": "trial" } }'
// 201
{ "contact": { "id": "…", "email": "jane@acme.com", "fields": { "company": "Acme", "plan": "trial" } } }
PATCH /v1/contacts/:contactId contacts:rw
Update a contact's custom fields. Body: { "fields": { … } }. Returns { "contact": { … } }.
GET /v1/contacts/:contactId/activities contacts:rw
The contact's activity timeline (form submissions, emails sent, API events). Returns { "activities": [ { "type", "data", "createdAt" } ] }.
Submissions
GET /v1/submissions submissions:r
Form submissions, newest first. Query params: formId (optional filter), limit (default 50, max 200), offset.
{ "submissions": [ { "id": "…", "formId": "…", "contactId": "…", "data": { "f_fld_email": "jane@acme.com" }, "createdAt": "…" } ] }
Events
POST /v1/events events:w
Ingest a custom event (e.g. a purchase completed in your backend) — usable as a custom_event test goal. Identify the visitor by fs_vid (the visitor cookie, best) or email (resolved via the contact's latest submission). Returns 202 { "accepted": true }; processing is asynchronous.
curl -X POST https://app.funnelsplit.com/v1/events \
-H "Authorization: Bearer fs_live_…" -H "content-type: application/json" \
-d '{ "name": "purchase", "fs_vid": "vid_from_cookie", "data": { "amount": 4900 } }'
Test stats
GET /v1/tests/:testId/stats tests:r
Live results for an A/B test: per-variant exposures, conversions, conversion rate and Bayesian probability-to-beat-control.
{ "test": { "id": "…", "name": "Hero copy", "status": "running", "mode": "fixed" },
"variants": [ { "id": "…", "letter": "A", "name": "Control (live)", "isControl": true }, … ],
"stats": { … } }
Workflow triggers
POST /v1/workflows/:workflowId/trigger workflows:trigger
Manually fire an active workflow. Body (all optional): contactId or email (to enter a specific contact), payload (available to workflow steps). Inactive workflows return 400 workflow_not_active. Returns 202 { "accepted": true }.
Inbound webhooks
Receive data from external systems (payment processors, CRMs). Create one under API & webhooks — you get a URL code and a signing secret. No API key is used; authentication is the HMAC signature.
POST /v1/hooks/in/:code HMAC signed
Send JSON with header X-FS-Signature = hex HMAC-SHA256 of the raw request body using the hook's secret. Invalid signatures get 401 with no side effects. What happens on receipt:
- The hook's field mapping extracts an email (dot-path, e.g.
data.customer.email) and upserts the contact with mapped fields. - Test goals bound to this hook log a conversion (include
fs_vidin the payload to attribute the visitor). - Workflows with an
inbound_webhooktrigger fire.
// Node.js signature
const crypto = require('node:crypto');
const body = JSON.stringify({ email: 'jane@acme.com', fs_vid: 'vid_…' });
const signature = crypto.createHmac('sha256', HOOK_SECRET).update(body).digest('hex');
await fetch('https://app.funnelsplit.com/v1/hooks/in/YOUR_CODE', {
method: 'POST',
headers: { 'content-type': 'application/json', 'X-FS-Signature': signature },
body,
});
// 202 { "accepted": true }
Outbound webhooks
Workflow outbound_webhook steps POST JSON to your URL with the same signature scheme in reverse:
- Header
X-FS-Signature= hex HMAC-SHA256(your endpoint's secret, raw body) — verify it before trusting the payload. - Non-2xx responses are retried 5× with exponential backoff (1 m → 2 m → 4 m → 8 m → 16 m), then marked dead — you can retry dead deliveries manually from the dashboard (Automate → Integrations → Webhook deliveries).
- Delivery timeout is 15 seconds; respond fast and process asynchronously.
Conversion pixel (external thank-you pages)
Record a test conversion from a thank-you page on any domain (ClickBank, carts, your own checkout). Create a custom_event goal on your test — the dashboard's test page shows a ready-made copy-paste snippet. It fires the event at your site's beacon endpoint (POST https://<your-site>/px/e, no API key needed — it's rate-limited and bot-filtered):
<script>
(function(){var h='https://YOUR-SITE.funnelsplit.com',q=new URLSearchParams(location.search),v=q.get('fsv');
try{if(v)localStorage.setItem('fs_vid',v);v=v||localStorage.getItem('fs_vid')}catch(e){}
if(!v){var m=document.cookie.match(/(?:^|; )fs_vid=([^;]*)/);v=m?m[1]:null}
var p=JSON.stringify({t:'custom',name:'EVENT_NAME',vid:v});
if(!(navigator.sendBeacon&&navigator.sendBeacon(h+'/px/e',p)))fetch(h+'/px/e',{method:'POST',body:p,mode:'no-cors',keepalive:true});
})();
</script>
- Cross-domain attribution: add
data-fs-decorateto the CTA link on your FunnelSplit page — the page runtime appends?fsv=<visitor-id>on click, and the pixel picks it up (and persists it in localStorage for multi-page funnels). - Same-domain thank-you pages need nothing extra (the
fs_vidcookie is read directly). - Without a resolvable visitor id the event still counts for the workspace but cannot be attributed to a test variant. Safari may limit localStorage retention.
Want an AI to drive all of this for you? The MCP endpoint exposes 39 tools — pages, layouts, forms, tests, contacts, workflows, analytics — to Claude and any MCP-capable assistant, using these same API keys.