Send up to 500 events at once
const url = 'https://api.threadkite.app/v1/events/batch';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"events":[{"contact":{"id":"2489E9AD-2EE2-8E00-8EC9-32D5F69181C0","externalId":"CUST-10442","integration":"my-dms","email":"sarah.turner@example.co.uk","phone":"07911 123456","name":"Sarah Turner"},"name":"order.paid","occurredAt":"2026-04-15T12:00:00Z","idempotencyKey":"my-dms:agreement:8812:ending-90","properties":{},"setFields":{"finance_end_date":"2027-01-03"},"timeline":{"show":true,"text":"Finance agreement ends on 3 January"},"createIfMissing":false,"threadId":"2489E9AD-2EE2-8E00-8EC9-32D5F69181C0","record":{"type":"invoice","externalId":"8812","label":"Invoice INV-8812","url":"https://dms.example.com/invoices/8812","status":"Paid","tone":"success","amount":{"value":289,"currency":"GBP"},"details":{"For":"Annual service","Due":"5 Nov 2026"},"integration":"my-dms"}}]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://api.threadkite.app/v1/events/batch \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "events": [ { "contact": { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "externalId": "CUST-10442", "integration": "my-dms", "email": "sarah.turner@example.co.uk", "phone": "07911 123456", "name": "Sarah Turner" }, "name": "order.paid", "occurredAt": "2026-04-15T12:00:00Z", "idempotencyKey": "my-dms:agreement:8812:ending-90", "properties": {}, "setFields": { "finance_end_date": "2027-01-03" }, "timeline": { "show": true, "text": "Finance agreement ends on 3 January" }, "createIfMissing": false, "threadId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "record": { "type": "invoice", "externalId": "8812", "label": "Invoice INV-8812", "url": "https://dms.example.com/invoices/8812", "status": "Paid", "tone": "success", "amount": { "value": 289, "currency": "GBP" }, "details": { "For": "Annual service", "Due": "5 Nov 2026" }, "integration": "my-dms" } } ] }'Each is handled as POST /v1/events, in order. One that is refused does not stop the rest: each result says whether it was recorded, or why not.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
object
Who it is about: their id, your id for them (externalId with integration), their email or their phone, tried in that order
object
Its id in your system (with integration)
Example
CUST-10442Your system, as used for external ids. Needed with externalId, except for an app (its external ids are always its own)
Example
my-dmsExample
sarah.turner@example.co.ukExample
07911 123456Their name, for a new contact
Example
Sarah Turnerobject.past_tense_verb (or one word). Recorded as event.<name>, the name flows, segments, webhooks and the event bus know it by; a name already starting event. is kept as it is
Example
order.paidWhen it happened; now when left out
Sending the same key again records nothing new and returns the first event
Example
my-dms:agreement:8812:ending-90What happened, as JSON (up to 16 KB). Kept for two years.
object
Example
{ "endDate": "2027-01-03", "monthlyPayment": 289, "balloon": 9450}Show it on the contact’s thread (also when its event type is set to show)
object
Add a line to the contact’s thread
The line’s words; the event type’s label when left out
Example
Finance agreement ends on 3 JanuaryWith an email or phone and nobody matching, add them as a new contact (an existing contact is never changed)
One of the contact’s threads it is about (else none)
The thing in your system it happened to (an invoice, an e-sign request, a document): added, or brought up to date, and shown as a card on the contact’s thread. An event older than the record’s state is kept but changes nothing
object
What kind of thing it is, in your words (invoice, esign, document, job_card). Picks its icon and groups it
Example
invoiceIts id in your system. With the type (and your app), identifies the record
Example
8812What staff see it called. Sent with every event about it
Example
Invoice INV-8812Where it opens in your system (https). Null clears it
Example
https://dms.example.com/invoices/8812Its status, in your words. Null clears it
Example
PaidHow the status looks: success, warning, danger or neutral (the default)
Example
successUp to 4 label and value pairs shown on its card, for people only (never used by automations). Replaces the details as a whole; null clears them
object
Example
{ "For": "Annual service", "Due": "5 Nov 2026"}Your system, when you are not an app (an app’s records are always its own). api when left out
Example
my-dmsResponses
Section titled “Responses”object
object
Its position in the batch, from 0
object
object
The name everywhere in Threadkite: event.<the name sent> for events from other systems
The event type’s label
internal (Threadkite), api, connection or flow
What it said (empty once cleared, two years after the event)
object
The line it added to the thread, if any
The idempotency key was used before: nothing new was recorded
A new contact was added (createIfMissing)
object
The app (its slug) or the integration that sent it
The app’s name, or the integration
The app that sent it, if an app did
The icon its type gets (link for types without their own)
object
In major units
object
The contacts it is about
The thread it shows on, if any (and if you can see it)
The inbox of the thread it shows on
When the event its current state came from happened
object
How many were recorded (duplicates included)
How many were refused
Example
{ "results": [ { "result": { "event": { "name": "event.order.paid", "label": "Finance agreement ending", "origin": "internal" }, "record": { "source": "acme-crm", "sourceName": "Acme CRM", "type": "invoice", "icon": "invoice", "externalId": "8812", "label": "Invoice INV-8812", "status": "Paid", "tone": "success", "amount": { "value": 289, "currency": "GBP" } } }, "error": { "code": "contact_not_found", "message": "No contact matches." } } ]}object
Stable machine-readable reason
With code invalid: every problem with the request, by field (message is the first)
object
Dotted path inside the item, e.g. attributes.price.amountMinor
Example
{ "statusCode": 403, "code": "forbidden", "message": "You do not have permission to do that.", "errors": [ { "message": "Enter the make" } ]}Missing or invalid API key
object
Stable machine-readable reason
With code invalid: every problem with the request, by field (message is the first)
object
Dotted path inside the item, e.g. attributes.price.amountMinor
Example
{ "statusCode": 403, "code": "forbidden", "message": "You do not have permission to do that.", "errors": [ { "message": "Enter the make" } ]}Sending events needs a plan with the API (code upgrade_required, feature api)
The organisation is on the Free plan (code upgrade_required)
object
Stable machine-readable reason
The feature that needs Pro
Where to upgrade or pay, in the web app
Example
{ "statusCode": 402, "code": "upgrade_required", "message": "Connections are part of Pro. Upgrade to connect texts, calls and more.", "feature": "extraSeats", "upgradePath": "/settings/billing"}The key does not have the scope (or the person the permission)
object
Stable machine-readable reason
With code invalid: every problem with the request, by field (message is the first)
object
Dotted path inside the item, e.g. attributes.price.amountMinor
Example
{ "statusCode": 403, "code": "forbidden", "message": "You do not have permission to do that.", "errors": [ { "message": "Enter the make" } ]}Rate limit reached
object
Stable machine-readable reason
Seconds until another request will be accepted
Example
{ "statusCode": 429, "code": "rate_limited", "message": "Too many requests. Each key can make 120 requests a minute; try again in 12 seconds.", "retryAfter": 12}