Skip to content

Tell Threadkite a customer bought

POST
/v1/sales
curl --request POST \
--url https://api.threadkite.app/v1/sales \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "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" }, "threadId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "dealId": "DEAL-8812", "integration": "my-dms", "value": 12500, "currency": "GBP", "itemId": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "itemExternalId": "example", "soldAt": "2026-04-15T12:00:00Z" }'

Records a sale for the sales reports, credited to the contact’s leads in the look-back (first and last touch). Say who bought (contact, or the threadId it was won in); without threadId it goes on the thread the same deal was recorded in, else the contact’s thread with the latest lead. The same dealId reported twice, or also linked to the thread as a won deal, counts once (duplicate: true). A new sale is sent to webhooks as sale.recorded (also to you: check actor to ignore your own).

Media typeapplication/json
object
contact

Who bought: their id, your id for them (externalId with integration), their email or their phone. Leave out with threadId.

object
id
string format: uuid
externalId

Its id in your system (with integration)

string
>= 1 characters <= 200 characters

Example

CUST-10442
integration

Your system, as used for external ids. Needed with externalId, except for an app (its external ids are always its own)

string
>= 1 characters <= 100 characters

Example

my-dms
email
string
<= 320 characters

Example

sarah.turner@example.co.uk
phone
string
<= 40 characters

Example

07911 123456
name

Their name, for a new contact

string
<= 200 characters

Example

Sarah Turner
threadId

The thread it was won in; otherwise the contact’s thread with the latest lead

string format: uuid
dealId

The deal’s id in your system: the same deal reported twice (or also a won deal link) counts once

string
>= 1 characters <= 200 characters

Example

DEAL-8812
integration

Your system, as for external ids; api when left out

string
>= 1 characters <= 100 characters

Example

my-dms
value

What it sold for, as a decimal amount

number

Example

12500
currency

ISO code; your organisation’s when left out

string
/^[A-Z]{3}$/

Example

GBP
itemId

The catalogue item sold

string format: uuid
itemExternalId

The item’s id in your system, instead of itemId

string
>= 1 characters <= 200 characters
soldAt

When it was sold; now when left out

string format: date-time
Media typeapplication/json
object
sale
required
object
id
required
string format: uuid
contactId
required
string format: uuid
threadId
required

The thread it was won in, if any

string format: uuid
nullable
dealId
required

The deal’s id in the system that recorded it, if known

string
nullable
integration
required
string
nullable
itemId
required
string format: uuid
nullable
valueMinor
required

What it sold for, in minor units of currency (pence for GBP)

number
nullable
currency
required
string
nullable
soldAt
required
string format: date-time
origin
required

How it was first recorded: outcome (closed as Won), link (a deal link marked won) or api (reported with POST /v1/sales)

string
Allowed values: outcome link api
userId
required

Who it is credited to

string
nullable
locationId
required
string format: uuid
nullable
firstSourceId
required

First touch: the earliest lead’s source

string format: uuid
nullable
lastSourceId
required

Last touch: the source of the latest lead before the sale

string format: uuid
nullable
leadsInLookback
required

Leads from the contact in the look-back

number
createdAt
required
string format: date-time
duplicate
required

The sale was recorded already (the same deal); nothing new was counted

boolean

Example

{
"sale": {
"dealId": "DEAL-8812",
"integration": "my-dms",
"valueMinor": 1250000,
"currency": "GBP",
"origin": "outcome"
}
}
Media typeapplication/json
object
statusCode
required
number
code
required

Stable machine-readable reason

string
message
required
string
errors

With code invalid: every problem with the request, by field (message is the first)

Array<object>
object
path
required

Dotted path inside the item, e.g. attributes.price.amountMinor

string
message
required
string

Example

{
"statusCode": 403,
"code": "forbidden",
"message": "You do not have permission to do that.",
"errors": [
{
"message": "Enter the make"
}
]
}

Missing or invalid API key

Media typeapplication/json
object
statusCode
required
number
code
required

Stable machine-readable reason

string
message
required
string
errors

With code invalid: every problem with the request, by field (message is the first)

Array<object>
object
path
required

Dotted path inside the item, e.g. attributes.price.amountMinor

string
message
required
string

Example

{
"statusCode": 403,
"code": "forbidden",
"message": "You do not have permission to do that.",
"errors": [
{
"message": "Enter the make"
}
]
}

The organisation is on the Free plan (code upgrade_required)

Media typeapplication/json
object
statusCode
required
number
code
required

Stable machine-readable reason

string
message
required
string
feature
required

The feature that needs Pro

string
nullable
Allowed values: extraSeats connections aiAssistant aiReplies rules fullReports api inboxes bulkMessages videoMessages
upgradePath
required

Where to upgrade or pay, in the web app

string

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)

Media typeapplication/json
object
statusCode
required
number
code
required

Stable machine-readable reason

string
message
required
string
errors

With code invalid: every problem with the request, by field (message is the first)

Array<object>
object
path
required

Dotted path inside the item, e.g. attributes.price.amountMinor

string
message
required
string

Example

{
"statusCode": 403,
"code": "forbidden",
"message": "You do not have permission to do that.",
"errors": [
{
"message": "Enter the make"
}
]
}

No contact or thread matches

Media typeapplication/json
object
statusCode
required
number
code
required

Stable machine-readable reason

string
message
required
string
errors

With code invalid: every problem with the request, by field (message is the first)

Array<object>
object
path
required

Dotted path inside the item, e.g. attributes.price.amountMinor

string
message
required
string

Example

{
"statusCode": 403,
"code": "forbidden",
"message": "You do not have permission to do that.",
"errors": [
{
"message": "Enter the make"
}
]
}

Rate limit reached

Media typeapplication/json
object
statusCode
required
number
code
required

Stable machine-readable reason

string
message
required
string
retryAfter
required

Seconds until another request will be accepted

number

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
}