API reference
Conversions API
Report conversions to SplitLab from your server, without adding a tracking snippet to your own pages.
Overview
SplitLab runs A/B tests on your landing pages. A conversion is counted for the variant the visitor saw. When the conversion happens on your side (a thank-you page, a booking, a sale, an audit completed in your own tool), your server can report it to SplitLab with a single HTTP call instead of loading our script.
You need two things: an API key from SplitLab, and the three visitor values SplitLab hands to your site.
How it works
- A visitor lands on a SplitLab page and is shown one variant of the test.
- When they move on to your site or your tool, SplitLab passes three values along with them:
sl_tid,sl_vidandsl_vh. - Your site keeps those values with the lead.
- When the conversion happens, your server sends them to the Conversions API.
- SplitLab counts the conversion for the variant that visitor saw.
| Value | Meaning |
|---|---|
sl_tid | The A/B test |
sl_vid | The variant the visitor saw |
sl_vh | The visitor |
Set up
There are no goals to create. Name each conversion with event_name (for example Lead or Purchase). The first time a test receives a name, SplitLab creates a goal for it on that test, and every later conversion with that name is counted under it, the way Facebook's Conversions API works. See Events and goals.
1. Get an API key. In SplitLab, open the client and go to API Keys → New API key. The key is shown once; keep it on your server. See Authentication.
2. If your tool opens inside the page (iframe), put the placeholders in the URL the iframe actually loads. That is the iframe's src, or its data-src when the page's script copies data-src into src (common for popups and lazy-loaded embeds). See Embedded tools (iframes). Links, buttons and forms need nothing.
3. Do it on every variant. Each variant has its own page, so the placeholders must be in each variant's HTML, or visitors on the variants without them can't be counted.
4. Test with the live test link. Open the test's shareable link in a private window, wait a couple of seconds, then go through your flow. Values are not filled in the page editor.
Getting the values
When a visitor leaves the SplitLab page for your domain, through a link, a button or a form, SplitLab adds the values to the URL automatically. Nothing to set up:
https://yoursite.com/thank-you?sl_tid=…&sl_vid=…&sl_vh=…- Forms that post to your server: the values are on that POST's URL as well. Read them there, because a redirect to your thank-you page may drop them.
- Forms sent with JavaScript (AJAX): the values are added to the form's
actionURL, so send the request to the form'sactionrather than a URL written into your script. - Thank-you page on the same domain as the SplitLab page: the values are only added when the visitor moves to a different domain. Use the email / phone fallback.
If your tool opens inside the SplitLab page instead, see Embedded tools (iframes).
Embedded tools (iframes)
A booking widget, quiz, calculator or audit tool that opens inside the SplitLab page (an iframe) is never navigated to, so SplitLab cannot add the values to its URL automatically. And because sl_vh is different for every visitor, the values cannot be typed into the page either.
Instead, write placeholders into the iframe's URL. Every time SplitLab serves the page, it replaces them with that visitor's values:
<iframe src="https://yourtool.com/start?sl_tid={{sl_tid}}&sl_vid={{sl_vid}}&sl_vh={{sl_vh}}"></iframe><iframe src="https://yourtool.com/start?sl_tid=4b8f0c1e-…&sl_vid=9a2d7e55-…&sl_vh=c3f1a9d2-…"></iframe>Inside your tool
- Read
sl_tid,sl_vidandsl_vhfrom the URL your tool was loaded with. - Keep them with the visitor's session or lead.
- When the visitor converts, send them from your server to the Conversions API.
const params = new URLSearchParams(window.location.search);
const splitlab = {
sl_tid: params.get('sl_tid'),
sl_vid: params.get('sl_vid'),
sl_vh: params.get('sl_vh'),
};
// Send these to your server with the lead; your server calls the Conversions API.Put the placeholders in the attribute the iframe really loads from. If the iframe has a data-src that the page's script copies into src when a popup opens, the placeholders go in data-src.
<iframe data-src="https://yourtool.com/start?sl_tid={{sl_tid}}&sl_vid={{sl_vid}}&sl_vh={{sl_vh}}"></iframe>Add them on every variant of the test. Each variant is its own page.
Rules
- Write the placeholders exactly as shown:
{{sl_tid}},{{sl_vid}},{{sl_vh}}, with no spaces inside the braces. - They work anywhere in the page's HTML, including
src,data-srcand links, and in embeds added to the page in SplitLab. - They are filled when SplitLab serves the test page. In the page editor they stay as written, which is expected.
- Only the iframes you add placeholders to receive the values; other embeds on the page get nothing.
Authentication
Every request carries your API key as a Bearer token. Create one in SplitLab: open the client, go to API Keys → New API key and give it a name. Keys look like sl_live_xxxxxxxx_… and are shown only once, so copy it straight away.
Authorization: Bearer YOUR_API_KEY- Keep the key on your server. Requests from a browser are rejected.
- A key only works for your own tests.
- Creating and revoking keys needs manager access to the client; anyone with access can see the list and when each key was last used.
- If a key is lost or exposed, create a new one, switch your server to it, then revoke the old one on the same page. A revoked key stops working at once. You can hold several keys at a time, so no conversions are lost in between.
Request
POST https://www.trysplitlab.com/api/v1/conversions
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"sl_tid": "4b8f0c1e-…",
"sl_vid": "9a2d7e55-…",
"sl_vh": "c3f1a9d2-…",
"event_name": "Lead"
}| Field | Required | Notes |
|---|---|---|
sl_vid + sl_vh | Yes, or email / phone | The values SplitLab passed to your site |
sl_tid | No | If sent, must belong to the same test as sl_vid |
email | Fallback | Any letter case |
phone | Fallback | Any format; only the digits are compared |
event_name | Recommended | What happened, for example Lead or Purchase. Up to 100 characters. Left out, API conversion is used. See Events and goals |
goal_id | No | Count the conversion on an existing goal instead. Takes priority over event_name |
Email / phone fallback
If the sl_* values were lost, send the email address or phone number the visitor entered in the form on the SplitLab page. SplitLab looks up their form submission from the last 90 days and counts the conversion for the variant they saw.
{ "email": "[email protected]" }{ "phone": "+1 (404) 555-0142" }This only works when the visitor filled in a form on the SplitLab page itself. Use it for the cases where the values cannot reach you, for example when your thank-you page is on the same domain as the SplitLab page (the values are only added when the visitor moves to a different domain).
Events and goals
Send an event_name and SplitLab does the rest. The first conversion with a new name creates a goal of that name on the test; later conversions with the same name are counted under it. Nothing has to be set up in SplitLab first.
{ "sl_vid": "…", "sl_vh": "…", "event_name": "Lead" }
{ "sl_vid": "…", "sl_vh": "…", "event_name": "Purchase" }- Each name becomes its own goal, shown in the test's goals with an API event label. Names are matched ignoring letter case, so
Leadandleadare the same goal. - Without
event_name, conversions are counted on a goal calledAPI conversion. - A goal is created per test. Sending
Leadfor two tests gives each test its ownLeadgoal. - The same visitor is counted once per event: a repeat returns
duplicate: true. A visitor can still count forLeadand laterPurchase. - In the results, conversions are counted once per visitor, whichever events they reached. The Goals column shows every event reached.
- To count a conversion on a goal that already exists instead (for example one set up in SplitLab), send its
goal_id, copied with the button next to the goal under Settings → Conversion Goals. A click or form goal is also recorded in the visitor's browser, so if the visitor already clicked, the API call returnsduplicate: true.
Responses
Every response is JSON. Errors include an error message saying what was wrong.
| Status | Meaning |
|---|---|
200 | Recorded: { ok, conversion_id, test_id, variant_id, goal_id, event_name, matched_by }. matched_by is sl_vid, email or phone |
200 | Already recorded for this visitor and event: { ok: true, duplicate: true, … }. Safe to ignore |
400 | Invalid payload (for example an empty event_name), or sl_tid doesn't match sl_vid |
401 | Missing, wrong or revoked API key |
403 | The test does not belong to your account |
404 | Unknown variant, no visit recorded for this visitor, or no form submission matched the email / phone |
422 | goal_id is not a goal of this test and variant |
429 | More than 60 requests a minute. Retry after the number of seconds in the Retry-After header |
Examples
curl -X POST https://www.trysplitlab.com/api/v1/conversions \
-H "Authorization: Bearer $SPLITLAB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sl_tid":"…","sl_vid":"…","sl_vh":"…","event_name":"Lead"}'const res = await fetch('https://www.trysplitlab.com/api/v1/conversions', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SPLITLAB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ sl_tid, sl_vid, sl_vh, event_name: 'Lead' }),
});
const result = await res.json(); // { ok: true, conversion_id, ... }import os, requests
res = requests.post(
"https://www.trysplitlab.com/api/v1/conversions",
headers={"Authorization": f"Bearer {os.environ['SPLITLAB_API_KEY']}"},
json={"sl_tid": sl_tid, "sl_vid": sl_vid, "sl_vh": sl_vh, "event_name": "Lead"},
timeout=10,
)
print(res.status_code, res.json())$ch = curl_init('https://www.trysplitlab.com/api/v1/conversions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('SPLITLAB_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['sl_tid' => $slTid, 'sl_vid' => $slVid, 'sl_vh' => $slVh, 'event_name' => 'Lead']),
]);
$result = json_decode(curl_exec($ch), true);Good to know
- Retries are safe. The same visitor and event are counted once; a repeat returns
duplicate: true. - Report late if you need to. The values stay valid, so a conversion that happens days later can still be sent.
- Where it shows. API conversions appear in the test's results in SplitLab. They don't add a row to the Leads list, because the visitor's form submission is already there.
- Server only. The API sends no CORS headers, so a call from browser code fails by design.
Troubleshooting
| You see | Check |
|---|---|
401 | The key is sent as "Authorization: Bearer …" and was copied in full |
404 No pageview | The sl_vid and sl_vh are from the same visit and were not edited. Values from a SplitLab preview link are not counted |
404 No lead matched | The email / phone is the one entered on the SplitLab page, within the last 90 days |
422 | The goal_id is not a goal of this test, or is limited to another variant. Leave goal_id out and send event_name instead |
200 with duplicate: true on the first call | You sent a goal_id of a click or form goal the visitor already reached in the browser. Send an event_name instead |
| Two goals for what should be one event | The names differ by more than letter case (Lead vs Leads). Use one exact name everywhere |
| A variant never gets API conversions | That variant's page is missing the placeholders. Add them on every variant |
{{sl_vid}} arrives literally | The iframe URL is on a page not served through a SplitLab test (an editor preview, for example). Check on the live test URL |
Still stuck? Contact us with the request body (without the key) and the response.