Developers
The Tellane API.
One REST surface — the same one the dashboard runs on — plus signed webhooks for what happens in your lanes and an MCP server for assistants. Base URL https://api.tellane.com.
Authentication
Create a key under Settings → API keys and send it as a bearer token. A live key (tl_live_…) can read and edit lanes, stops, screens, and leads — the routes marked LIVE KEY below. A test key (tl_test_…) can only read. Keys belong to one workspace and can never touch the team, billing, integrations, or other keys.
curl https://api.tellane.com/w/{workspace}/lanes \
-H "Authorization: Bearer tl_live_…"{workspace} is your workspace slug — the subdomain of your public address. Errors are JSON: { "error": "…", "code": "…" } with the usual status (400 invalid, 401 no or bad key, 403 not allowed for this key, 404, 409 conflict, 429).
Rate limits
600 requests per minute per key. Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (unix seconds); past the limit you get a 429 with Retry-After.
Lanes
A lane is an interactive demo. Every lane route is under your workspace.
| Route | What it does |
|---|---|
GET/w/{workspace}/lanes | Every lane with status, stop count, and timestamps. |
POST/w/{workspace}/lanesLIVE KEY | Create a draft lane.{ "name": "Homepage tour" } |
GET/w/{workspace}/lanes/{id} | One lane with its stops and screens (also GET …/lanes/slug/{slug}). |
PATCH/w/{workspace}/lanes/{id}LIVE KEY | Rename, publish, archive, set the CTA, require an email, tune the player.{ "status": "live", "displayTitle": "See Acme in 3 minutes", "ctaUrl": "https://…", "ctaLabel": "Start a free trial", "ctaAlways": true, "requireEmail": false, "autoplaySeconds": 5, "loop": false, "mobileMinWidth": 0, "mobileWarning": null, "voice": null, "accentColor": "#2a5fd6", "frame": "dark", "background": null, "stopNav": true, "privateAccess": false, "musicVolume": 20 }status: draft | live | archived. Going live fires lane.published. displayTitle is what buyers see — the player’s title bar, the browser tab, the share card (≤ 120 chars, empty string or null falls back to the lane’s own name, which stays internal to the dashboard). ctaAlways puts the CTA button in the player chrome at every stop instead of only on the end card (it needs a ctaUrl to do anything); ctaLabel names it (≤ 40 chars, empty string or null falls back to “Book a demo”). autoplaySeconds (2–30, out-of-range is a 400, null = off) autoplays the lane once the viewer presses play; loop restarts it at the end while autoplaying. mobileMinWidth (0–2000, 0 = off) warns viewers of embeds narrower than that many pixels; mobileWarning replaces the default copy (≤ 200 chars, empty string clears it). voice picks the lane's AI voiceover (alloy | echo | fable | onyx | nova | shimmer, null = off). Demo theme: accentColor and background are hex (null follows the workspace accent / the frame's own backdrop), frame is light | dark, stopNav lets viewers click the progress rail to jump between stops, privateAccess limits the lane to signed-in members of the workspace (the public payload answers 403 PRIVATE_LANE to everyone else), musicVolume (0–100) is how loud the background bed plays. |
PUT/w/{workspace}/lanes/{id}/stopsLIVE KEY | Replace the whole stop list (1–50 stops, in order).[{ "name": "Welcome", "screenId": null, "hotspotX": 50, "hotspotY": 50, "title": "…", "body": "…", "branch": null, "leadForm": false, "hotspotOnly": false, "zoom": false, "zoomRect": null, "kind": "hotspot", "emphasis": "none", "ctas": null, "tipPosition": "auto", "tipWidth": null, "tipStyle": null, "showStep": true, "backArrow": false, "hidePointer": false, "hideOnMouseOut": false, "voiceText": null }]hotspotOnly hides the tooltip's Next button (the viewer clicks the hotspot); zoom opens the stop on the whole screen and then eases into zoomRect — {x, y, w, h} as percentages of the screen, no side under 12, and it has to stay on the screen. Optional zoomRect.followGuide centers that rectangle on the hotspot at playback, clamped to screen edges; omitted or false keeps a fixed region. Both default to false, and zoomRect defaults to null, which frames the hotspot. The guide: kind is hotspot | modal (a modal is a card over the middle of the screen, with no pointer); emphasis is none | highlight | backdrop | spotlight; ctas is up to two { label (≤ 40), url, variant: primary | secondary } buttons that leave for a URL and beacon a cta event; tipPosition is auto | top | right | bottom | left; tipWidth is 180–560 px (null = responsive); tipStyle is { background, border, hotspot (hex), font: display | body | mono } and every field inherits the lane's theme when unset; showStep (default true), backArrow, hidePointer and hideOnMouseOut are the per-stop config. voiceText (≤ 500 chars, empty string clears it) overrides what the voiceover reads on this stop — by default it narrates the body, falling back to the title. |
POST/w/{workspace}/lanes/{id}/duplicateLIVE KEY | Copy a lane with its stops and screens. |
DELETE/w/{workspace}/lanes/{id}LIVE KEY | Delete a lane and everything in it. |
GET/w/{workspace}/lanes/{id}/links | Prospect links (personalized URLs) for a lane. |
POST/w/{workspace}/lanes/{id}/linksLIVE KEY | Mint a prospect link; open the lane with ?for={token}.{ "prospectName": "Cobalt Health", "prospectDomain": "cobalthealth.com" } |
DELETE/w/{workspace}/lanes/{id}/links/{linkId}LIVE KEY | Retire a prospect link. |
Screens
Captured screens are images on a lane; every uploaded screen also becomes a stop at the end of the lane.
| Route | What it does |
|---|---|
GET/w/{workspace}/lanes/{id}/screens | Screens with their URLs, dimensions, kind (image | html), painted-over areas (redactions) and, while there are any, the scrubbed copy's URL (redactedUrl). |
POST/w/{workspace}/lanes/{id}/screensLIVE KEY | multipart/form-data: files[] (PNG/JPEG/WebP/GIF, ≤ 10 MB each, ≤ 30 per request, ≤ 80 per lane), labels[], source. |
PATCH/w/{workspace}/lanes/{id}/screens/{screenId}LIVE KEY | Relabel a screen.{ "label": "Pipelines" } |
DELETE/w/{workspace}/lanes/{id}/screens/{screenId}LIVE KEY | Remove a screen. |
GET/w/{workspace}/lanes/{id}/screens/{screenId}/source | The capture's own bytes (an HTML capture comes back as text/plain) — the copy the editor paints over. Never cached. |
PUT/w/{workspace}/lanes/{id}/screens/{screenId}/redactionsLIVE KEY | Paint over parts of a screen. multipart/form-data: redactions (JSON: up to 40 { x, y, w, h, mode: blur | solid } areas in % of the screen, no side under 0.5) and file — the same screen with those areas scrubbed: pixelated or blocked out for an image, the text under them replaced for an HTML capture. The editor produces that copy; the API validates it like an upload. The public player then loads the scrubbed copy and paints the areas over it, and the capture itself moves to an address only this API returns. An empty list clears everything (no file needed). |
DELETE/w/{workspace}/lanes/{id}/screens/{screenId}/redactionsLIVE KEY | Clear every painted-over area; the public player goes back to the capture itself. |
POST/w/{workspace}/lanes/{id}/musicLIVE KEY | multipart/form-data: file (MP3/M4A/OGG/WAV, ≤ 8 MB) — the lane's looping background bed. Replaces the previous track. |
DELETE/w/{workspace}/lanes/{id}/musicLIVE KEY | Remove the background music. |
Leads
People who filled in a form mid-lane, with the engagement of their own drive.
| Route | What it does |
|---|---|
GET/w/{workspace}/leads | List, newest first. Query: q, status (new|qualified|synced), intent (high|medium|low), lane (id), sort (newest|score), page (0-based), size (≤ 100). Returns rows, total, and workspace-wide stats. |
GET/w/{workspace}/leads/{id} | One lead plus drive: the stops they reached, timestamps, device, source, prospect link. |
PATCH/w/{workspace}/leads/{id}LIVE KEY | Change status or notes.{ "status": "qualified", "notes": "Call Tuesday" } |
DELETE/w/{workspace}/leads/{id}LIVE KEY | Delete a lead (drive analytics stay). |
PATCH/w/{workspace}/leadsLIVE KEY | Bulk status change.{ "ids": ["…"], "status": "synced" } |
DELETE/w/{workspace}/leadsLIVE KEY | Bulk delete.{ "ids": ["…"] } |
GET/w/{workspace}/leads/export.csv | Every matching lead as CSV (same filters as the list, or ids=). |
Analytics
Everything the Analytics page shows, as JSON.
| Route | What it does |
|---|---|
GET/w/{workspace}/analytics | KPIs vs the previous period, daily series, funnel, day×hour heatmap, sources, devices, countries, referrers, accounts, per-lane totals, per-stop reach, prospect links, recent drives. Query: range (7d|30d|90d, default 30d), lane (id). |
GET/w/{workspace}/analytics/live | Sessions active in the last 30 seconds. |
Public routes
No key needed — these are what the player itself calls, useful if you embed lanes your own way.
| Route | What it does |
|---|---|
GET/p/{workspace}/{laneSlug} | The player payload for a live lane: stops, screens (kind, redactions, and url — the scrubbed copy while areas are painted over), accent, tracking ids, player settings (autoplaySeconds, loop, mobileMinWidth, mobileWarning, voice — stops carry audioUrl narration clips while a voice is set); ?for={token} adds the prospect. |
POST/e | Player beacon: up to 100 events per call (play, stop, cta, lead, complete) with session and visitor ids. The player sends these for you. |
POST/leads | The lead form: workspaceId, laneId, sessionId, email, optional name/company/answers. 20 per minute per IP. |
Webhooks
Register https endpoints under Settings → Webhooks. Each event is one JSON POST: { "id", "event", "createdAt", "attempt", "data" }, retried five times on a 1 m → 5 m → 30 m → 2 h → 12 h backoff until your endpoint answers 2xx within 10 seconds. The delivery log in Settings shows every attempt.
| Event | Fires when |
|---|---|
lane.played | a drive starts — one per play beacon, so expect volume |
lane.completed | a drive reaches the end of a lane |
lead.captured | a form is submitted mid-lane (new lead) |
lane.published | a lane goes from draft to live |
Headers: X-Tellane-Event, X-Tellane-Delivery, X-Tellane-Timestamp, and X-Tellane-Signature: t=<unix>,v1=<hex> — an HMAC-SHA256 over <t>.<raw body> with the endpoint's secret. Verify it before trusting a delivery:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, header, rawBody, maxAgeSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > maxAgeSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex"), b = Buffer.from(parts.v1 ?? "", "hex");
return a.length === b.length && timingSafeEqual(a, b);
}MCP
POST https://api.tellane.com/mcp speaks the Model Context Protocol (Streamable HTTP, stateless) and authenticates with an API key — a test key is enough, every tool is read-only: workspace_info, list_lanes, get_lane, analytics_summary, list_leads. Claude Desktop, Claude Code, Cursor, ChatGPT, and Copilot all take this shape:
{
"mcpServers": {
"tellane": {
"url": "https://api.tellane.com/mcp",
"headers": {
"Authorization": "Bearer tl_test_YOUR_KEY"
}
}
}
}Questions, or a route you need that isn't here? [email protected]