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.

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://app.leadtosheet.com/api/v1/submissions \
  -H "Authorization: Bearer lts_your_key_here"

Or from Node 18+:

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

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

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 or incomplete 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 (ISO-8601), search (free text across URL, UTM, and field values).
Request
curl "https://app.leadtosheet.com/api/v1/submissions?per_page=2&sync_status=synced" \
  -H "Authorization: Bearer lts_your_key_here"
Response
{
  "data": [
    {
      "id": "sub_01HW6...",
      "form_id": "form_01HW5...",
      "site_id": "site_01HW4...",
      "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://app.leadtosheet.com/api/v1/submissions/sub_01HW6abc \
  -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, or omit it and provide a stable form.key so LeadToSheet can create or reuse an API form for server-side submissions.

Request
curl https://app.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",
    "utm": { "source": "newsletter", "medium": "email" },
    "click_ids": { "gclid": "EAIaIQobCh..." }
  }'
Response
HTTP/1.1 201 Created
Content-Type: application/json

{
  "id": "sub_01HW6new...",
  "form_id": "form_01HW5...",
  "site_id": "site_01HW4...",
  "submitted_at": "2026-05-03T11:02:14.000Z",
  "synced_at": null,
  "sync_status": "pending",
  "source": "api",
  "is_spam": false,
  "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."
}

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
Optional ignored filter to include or exclude archived forms.
Request
curl https://app.leadtosheet.com/api/v1/forms \
  -H "Authorization: Bearer lts_your_key_here"
Response
{
  "data": [
    {
      "id": "form_01HW5...",
      "site_id": "site_01HW4...",
      "scope_key": "site",
      "fingerprint": "a1b2c3d4",
      "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 }
}

Update form metadata

PATCH/api/v1/forms/{id}scopeforms:write
Update name, sheet_tab_name, or ignored. Other fields are read-only.
Request
curl -X PATCH https://app.leadtosheet.com/api/v1/forms/form_01HW5... \
  -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://app.leadtosheet.com/api/v1/site \
  -H "Authorization: Bearer lts_your_key_here"
Response
{
  "id": "site_01HW4...",
  "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"
  }
}

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