threadliner

Code examples

Five common integration tasks, each with a real request and response.

Every request/response body on this page was run against a live Threadliner API instance and copied verbatim — not hand-typed. See Authentication for how to get an API key, and Error response format for what failures look like.

Create a contact

The one endpoint most integrations start with — everything else (forms, lists, deals) attaches to a contact record.

curl -X POST https://api.threadliner.io/contacts \
  -H "Authorization: Bearer tl_live_..." \
  -H "Content-Type: application/json" \
  -d '{"email":"jane@example.com","firstName":"Jane","lastName":"Doe"}'
201 response
{
  "id": "cms7wqpka00068o3zyl49e2dn",
  "email": "jane@example.com",
  "firstName": "Jane",
  "lastName": "Doe",
  "phone": null,
  "company": null,
  "customFieldValues": {},
  "tags": [],
  "emailSubscribedStatus": "NONE",
  "lastActivityAt": null,
  "source": null,
  "createdAt": "2026-07-30T19:29:32.650Z",
  "updatedAt": "2026-07-30T19:29:32.650Z"
}

Full field reference: Create a contact.

Submit a form

Unauthenticated — this is what a form embed itself calls, so it never carries an API key. Field IDs come from the form's own schema (GET /public/forms/:id); this example matches a two-field form with IDs email and firstName.

curl -X POST https://api.threadliner.io/public/forms/cms7wtd4w000o8o3za23a9k2s/submit \
  -H "Content-Type: application/json" \
  -d '{"values":{"email":"lead@example.com","firstName":"Lee"}}'
201 response
{
  "success": true,
  "submissionId": "cms7wtd88000s8o3zz79xxpf0"
}

More on mapping fields to contact fields and conditional logic: Forms.

Create a deal

Requires an existing pipelineId (list pipelines) and contactId. stageId is optional — omitted here, so it defaults to the pipeline's first stage.

curl -X POST https://api.threadliner.io/deals \
  -H "Authorization: Bearer tl_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "pipelineId": "cms7wsqyq000c8o3zyy8rolvj",
    "contactId": "cms7wqpka00068o3zyl49e2dn",
    "name": "Jane Doe - Annual Plan",
    "valueCents": 120000
  }'
201 response
{
  "id": "cms7wsr11000i8o3zgli0gkno",
  "pipelineId": "cms7wsqyq000c8o3zyy8rolvj",
  "stageId": "cms7wsqyq000d8o3z7mvvvbxt",
  "contactId": "cms7wqpka00068o3zyl49e2dn",
  "name": "Jane Doe - Annual Plan",
  "valueCents": 120000,
  "status": "OPEN",
  "closedAt": null,
  "createdAt": "2026-07-30T19:31:07.861Z",
  "updatedAt": "2026-07-30T19:31:07.861Z"
}

valueCents is in cents (120000 = $1,200.00) — same convention used throughout CRM.

Register a webhook

eventTypes accepts any of the event types listed here. Like an API key, the secret is only ever returned in this response — save it, it's what you use to verify each delivery's signature.

curl -X POST https://api.threadliner.io/webhooks \
  -H "Authorization: Bearer tl_live_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/threadliner","eventTypes":["contact.created"]}'
201 response
{
  "id": "cms7ws6n8000a8o3zqq4fc4yl",
  "url": "https://example.com/hooks/threadliner",
  "eventTypes": ["contact.created"],
  "enabled": true,
  "createdAt": "2026-07-30T19:30:41.444Z",
  "secret": "_y4SwpCiP3z0LzSQ5Wn5Db1P0snrcejC"
}

Deliveries are single-attempt — there's no automatic retry on failure. See Webhooks for delivery status/error visibility.

Send a campaign

Sends immediately to every confirmed subscriber on the campaign's list — there's no scheduling. campaignId comes from creating a campaign first.

curl -X POST https://api.threadliner.io/campaigns/cms7wtteg00108o3zmyt1oi4a/send \
  -H "Authorization: Bearer tl_live_..."
201 response
{ "queued": 1 }

queued is the number of confirmed subscribers the campaign was queued to send to. If the list has no confirmed subscribers yet, this fails instead:

400 response
{
  "statusCode": 400,
  "message": "List has no subscribed recipients",
  "error": "Bad Request"
}
Was this page helpful?

Last updated on

On this page