AppSumoAppSumo Lifetime Deal starting at $39
Get the Deal Now!
Referencev1Stable

LeadToSheet REST API

A small, well-mannered REST API over JSON. Per-site bearer tokens, snake_case fields, predictable pagination, and a machine-readable OpenAPI 3.1 spec.

What you can do with the API

Use the REST API to read submissions and form metadata, post leads from a server-side workflow, or update form names and destination tabs from your own admin tooling. The API exposes the stable lead and form fields integrations need while keeping internal classifier feedback private.

Per-site Bearer auth

Each key is scoped to a single site. Rotate or revoke instantly from Settings → API.

Page-based pagination

Predictable page and per_page query params with a pagination object on every list response.

Granular scopes

Read or write submissions and forms separately. Fail safe with insufficient_scope on mismatch.

OpenAPI 3.1 spec

Drop the spec into Postman, Stoplight, or any code generator and you're live in minutes.

MCP server

Expose the same scoped REST actions to AI agents over the Model Context Protocol at /api/mcp.

Quick start

In four steps, you'll have a working request against your own site:

  1. Open Settings → API for the site you want to access.
  2. Click Create API key, name it, and pick the scopes it needs.
  3. Copy the key - you'll only see it once - and store it in your secrets manager.
  4. Make a request:
curl
curl https://www.leadtosheet.com/api/v1/submissions \
  -H "Authorization: Bearer lts_your_key_here"

Or from Node 18+:

quick-start.js
async function main() {
  const res = await fetch(
    "https://www.leadtosheet.com/api/v1/submissions",
    { headers: { Authorization: "Bearer lts_your_key_here" } }
  );

  const { data } = await res.json();
  console.log(data);
}

main();

Authentication

Every request needs an Authorization: Bearer <key> header. Keys are scoped to a single site - there is no separate site_id to pass.

We store only an HMAC-SHA256 hash of each key, so we can never recover one if you lose it. Revoke and recreate instead.

Scopes

Each key carries one or more scopes. Pick the minimum set for what your integration needs:

ScopeGrants
submissions:readList and read submissions
submissions:writeCreate new submissions (programmatic ingest)
forms:readList and read forms
forms:writeUpdate form metadata (rename, ignore, change sheet tab)

Errors

Errors always have the same shape so you can match on error.code in your client:

http
HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not have the required scope: submissions:write."
  }
}
StatusCodeMeaning
401unauthenticatedMissing or malformed Authorization header.
401invalid_keyKey not recognized, revoked, or site inactive.
403insufficient_scopeKey is valid but missing the required scope.
404not_foundResource doesn't exist on this site.
422validation_errorBody or query failed validation. Check error.details.
429quota_exceededSubmission quota is exhausted for the current period.
403subscription_inactiveThe site account is canceled, incomplete, or incomplete-expired and cannot accept submissions.
403trial_expiredThe trial has expired and the account must upgrade before collecting submissions.

Pagination

List endpoints accept page (default 1) and per_page (default 25, max 100). Responses always include a pagination object with page, per_page, total, and total_pages.

json
{
  "data": [ /* … */ ],
  "pagination": {
    "page": 1,
    "per_page": 25,
    "total": 142,
    "total_pages": 6
  }
}

Submissions

Anything captured via the SDK or a form endpoint becomes a submission. Read them, filter them, or post your own.

List submissions

GET/api/v1/submissionsscopesubmissions:read
Filters: form_id, sync_status (pending / synced / failed), is_spam, from, to (date YYYY-MM-DD or full ISO-8601 date-time; date-only values snap to 00:00:00.000Z for from and 23:59:59.999Z for to), search (free text across URL, utm.source / utm.medium / utm.campaign, and field values; utm.term and utm.content are not searched). Results are ordered by submitted_at descending.
Request
curl "https://www.leadtosheet.com/api/v1/submissions?per_page=2&sync_status=synced" \
  -H "Authorization: Bearer lts_your_key_here"
Response
{
  "data": [
    {
      "id": "clmcvxqjz0000abcd1234efgh",
      "form_id": "clmcvxq000001abcd1234efgh",
      "site_id": "clmcvxq000002abcd1234efgh",
      "submitted_at": "2026-05-03T10:14:22.000Z",
      "synced_at": "2026-05-03T10:14:23.812Z",
      "sync_status": "synced",
      "source": "sdk",
      "is_spam": false,
      "url": "https://acme.com/contact",
      "referrer": "https://google.com/",
      "fields": {
        "email": "jane@example.com",
        "message": "Interested in your enterprise plan."
      },
      "utm": {
        "source": "google",
        "medium": "cpc",
        "campaign": "spring_2026",
        "term": null,
        "content": null
      },
      "click_ids": {
        "gclid": "EAIaIQobCh...",
        "fbclid": null,
        "msclkid": null,
        "ttclid": null,
        "li_fat_id": null
      }
    }
  ],
  "pagination": { "page": 1, "per_page": 2, "total": 1, "total_pages": 1 }
}

Get a submission

GET/api/v1/submissions/{id}scopesubmissions:read
Returns the full record including UTM and click ID attribution.
Request
curl https://www.leadtosheet.com/api/v1/submissions/clmcvxqjz0000abcd1234efgh \
  -H "Authorization: Bearer lts_your_key_here"

Create a submission

POST/api/v1/submissionsscopesubmissions:write
Use this for server-side ingest alongside or instead of the SDK.

Send an existing form_id to attach the submission to a known form, or omit it. When form_id is omitted, pass an optional form.key (or form.name) to upsert a stable API form per identity; if you also omit those, submissions land on a single shared default API form.

Request
curl https://www.leadtosheet.com/api/v1/submissions \
  -H "Authorization: Bearer lts_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "form": { "key": "server-contact", "name": "Server Contact" },
    "fields": {
      "email": "jane@example.com",
      "name": "Jane Doe",
      "message": "Hello!"
    },
    "url": "https://acme.com/contact",
    "referrer": "https://news.ycombinator.com/",
    "utm": { "source": "newsletter", "medium": "email" },
    "click_ids": { "gclid": "EAIaIQobCh..." }
  }'
Response
HTTP/1.1 201 Created
Content-Type: application/json

{
  "id": "clmcvxqjz0003abcd1234efgh",
  "form_id": "clmcvxq000001abcd1234efgh",
  "site_id": "clmcvxq000002abcd1234efgh",
  "submitted_at": "2026-05-03T11:02:14.000Z",
  "synced_at": null,
  "sync_status": "pending",
  "source": "api",
  "is_spam": false,
  "url": "https://acme.com/contact",
  "referrer": "https://news.ycombinator.com/",
  "fields": { "email": "jane@example.com", "name": "Jane Doe", "message": "Hello!" },
  "utm": { "source": "newsletter", "medium": "email", "campaign": null, "term": null, "content": null },
  "click_ids": { "gclid": "EAIaIQobCh...", "fbclid": null, "msclkid": null, "ttclid": null, "li_fat_id": null }
}
Duplicate response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "duplicate": true,
  "message": "An identical submission was received within the dedupe window and was not stored."
}
Spam response
HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "accepted": true,
  "classified_as": "spam"
}

Forms

Forms are detected automatically from the SDK, public API, or first endpoint submission. Use these to inspect or rename them, or to retarget them at a different sheet tab.

List forms

GET/api/v1/formsscopeforms:read
Returns both ignored and non-ignored forms by default. Pass ?ignored=true to return only ignored forms or ?ignored=false to return only active ones. Results are ordered by last_seen_at descending.
Request
curl https://www.leadtosheet.com/api/v1/forms \
  -H "Authorization: Bearer lts_your_key_here"
Response
{
  "data": [
    {
      "id": "clmcvxq000001abcd1234efgh",
      "site_id": "clmcvxq000002abcd1234efgh",
      "scope_key": "site:clmcvxq000002abcd1234efgh",
      "fingerprint": "1a2b3c4d5e6f7890",
      "action": "/contact",
      "method": "POST",
      "name": "Contact form",
      "page_name": "Home",
      "form_type": "contact",
      "field_keys": ["email", "name", "message"],
      "sheet_tab_name": "Contact",
      "ignored": false,
      "last_seen_at": "2026-05-03T10:14:22.000Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 25, "total": 1, "total_pages": 1 }
}

Get a form

GET/api/v1/forms/{id}scopeforms:read
Returns a single form, including ignored ones.
Request
curl https://www.leadtosheet.com/api/v1/forms/clmcvxq000001abcd1234efgh \
  -H "Authorization: Bearer lts_your_key_here"

Update form metadata

PATCH/api/v1/forms/{id}scopeforms:write
Update name, sheet_tab_name, or ignored. The body is strict - any other field in the request is rejected with 422 validation_error.
Request
curl -X PATCH https://www.leadtosheet.com/api/v1/forms/clmcvxq000001abcd1234efgh \
  -H "Authorization: Bearer lts_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Renamed form",
    "sheet_tab_name": "Inbound leads",
    "ignored": false
  }'

Site

A single endpoint that returns the site identity and current month's usage. Useful for surfacing plan limits in your own admin tooling.

GET/api/v1/sitescopeany read scope
Returns site metadata (name, domain, sheet ID) plus the current period's submission usage.
Request
curl https://www.leadtosheet.com/api/v1/site \
  -H "Authorization: Bearer lts_your_key_here"
Response
{
  "id": "clmcvxq000002abcd1234efgh",
  "name": "Acme Marketing Site",
  "domain": "acme.com",
  "google_sheet_id": "1aBc...",
  "google_sheet_url": "https://docs.google.com/spreadsheets/d/1aBc...",
  "is_active": true,
  "created_at": "2026-04-01T08:00:00.000Z",
  "usage": {
    "subscription_plan": "GROWTH_MONTHLY",
    "subscription_status": "ACTIVE",
    "monthly_submission_count": 142,
    "current_period_end": "2026-06-01T00:00:00.000Z"
  }
}

MCP server

LeadToSheet exposes the same site-scoped API surface as an MCP server for AI agents and internal agent tooling. The endpoint is /api/mcp, using Streamable HTTP and the same bearer token you create in Settings → API.

POST/api/mcpscopetool-dependent scopes
MCP tools delegate to the REST API, so submissions and forms keep the same scope checks, validation, and response shapes.
Initialize request
curl https://www.leadtosheet.com/api/mcp \
  -H "Authorization: Bearer lts_your_key_here" \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-06-18",
      "capabilities": {},
      "clientInfo": { "name": "my-agent", "version": "1.0.0" }
    }
  }'
ToolScopeWhat it does
get_sitesubmissions:read or forms:readRead site identity and current usage.
list_submissionssubmissions:readSearch and page through captured submissions.
get_submissionsubmissions:readFetch one submission by ID.
create_submissionsubmissions:writeCreate a lead through the API ingestion path.
list_formsforms:readList forms discovered on the authenticated site.
get_formforms:readFetch one form by ID.
update_formforms:writeRename a form, change its sheet tab, or ignore it.

OpenAPI spec

The full machine-readable spec lives at /api/v1/openapi.json (OpenAPI 3.1). Drop it into Postman, Insomnia, Stoplight, or any code generator.

Postman

File → Import → Link, paste the spec URL.

Stoplight Studio

New Project → Import → API URL.

openapi-generator

openapi-generator-cli generate -i <url> -g typescript-fetch

Insomnia

Application → Import → From URL.

Next

Form endpoints

Drop-in HTTP POST URLs for any HTML form - no SDK required.

Read reference

Need help?

Help Center

Step-by-step guides for setup, syncing, billing, and troubleshooting.

Browse help articles