Funnel/Split

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"

Scopes

Each key carries an explicit set of scopes. A request needing a scope the key lacks gets 403 insufficient_scope.

Data API scopes

ScopeUnlocks
contacts:rwRead, create and update contacts; read activity timelines
submissions:rRead form submissions
events:wIngest custom conversion events
tests:rRead A/B test stats
workflows:triggerManually fire active workflows

AI / content-control scopes (MCP)

These scopes power the MCP endpoint, which lets AI assistants build and manage content:

ScopeUnlocks (via MCP tools)
pages:rwCreate, edit, version, publish and unpublish pages
forms:rwCreate, edit and publish forms; read them with warnings
tests:rwFull A/B test control: variants, goals, start/pause/complete, split links
workflows:rwCreate, edit, activate and pause workflows; read run logs
analytics:rRead traffic rollups and funnel stats
templates:rwList and clone page/form/workflow templates
media:rList media-library assets and their URLs

Errors & rate limits

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:

  1. The hook's field mapping extracts an email (dot-path, e.g. data.customer.email) and upserts the contact with mapped fields.
  2. Test goals bound to this hook log a conversion (include fs_vid in the payload to attribute the visitor).
  3. Workflows with an inbound_webhook trigger 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:

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>

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.

No credit card required

Ship the page
that wins.

Start free See pricing