English
English
English
English
One endpoint: every sale SalesDash holds, with the agent and the proposition it was booked on, in the order in which they last changed. It is built for a periodic poll — remember when you last asked, ask again for everything that changed since, and your own copy stays current. That is the same way SalesDash's own providers read from the systems they poll.
GET /sales lists sales, newest change last, from a moment you choose.GET /integrations/api/v1/sales?updated_from=2026-10-27T06:00:00Z&limit=500| Parameter | Required | Notes |
|---|---|---|
updated_from | no | Only sales changed at or after this moment. Leave it out for the whole history. |
updated_to | no | Only sales changed at or before this moment. |
cursor | no | Continue after a page — see paging. The next value of the previous call, as-is. |
limit | no | Sales per call, 1 to 500. Default 100. |
Both bounds are inclusive. A moment is written in UTC, to the second, with a Z: 2026-10-27T06:00:00Z. Every timestamp in a response uses the same format, so a value you received can go straight back in. Any other form is refused.
{
"sales": [
{
"id": 1,
"external_id": "1784904509",
"provider_id": 2,
"sold_at": "2026-07-24T08:27:27Z",
"cancelled_at": null,
"is_retention": null,
"contract_value_cents": 190100,
"total_commission_cents": 0,
"contact_key": null,
"meta": {
"connection_count": 2,
"electricity_usage": 1300,
"electricity_return": 100,
"gas_usage": 0
},
"team_id": 1,
"agent": {
"id": 1,
"name": "Olivia Stevens",
"team_id": 1,
"is_active": true
},
"proposition": {
"id": 3,
"name": "Omnis 3 years",
"project_id": null,
"is_active": true
},
"created_at": "2026-07-24T08:58:27Z",
"updated_at": "2026-07-24T08:58:27Z",
"deleted_at": null
}
],
"has_more": true,
"next": "eyJ1IjoiMjAyNi0wNy0yNFQwODo1ODoyN1oiLCJpIjoxfQ"
}| Field | Meaning |
|---|---|
id | SalesDash's own id for the sale. Stable for the sale's whole life — key your copy on it. |
external_id | The id the sale has in the system it came from, if it came from a provider. |
provider_id | The provider that delivered it, or null for a sale entered by hand. |
sold_at | When the sale was sold. null for a sale that is not (yet) counted as sold. |
cancelled_at | When the sale was cancelled, or null. |
is_retention | true for a retention sale, false for a new one, null when the source did not say. |
contract_value_cents | The contract value, in cents — 190100 is €1.901,00. |
total_commission_cents | The commission SalesDash calculated for this sale from your commission rules, in cents. 0 when no rule applies. |
contact_key | The key that ties this sale to the contact it was made with, as the provider delivered it. |
agent_id, proposition_id | The ids behind the agent and proposition blocks below. They are here as well because a block is null both for a sale that was never linked and for one whose agent or proposition has since been deleted — the id is what still tells you which one it was. |
meta | Everything the provider stored with the sale — energy consumption, product lines, a telecom bundle. Its shape depends on the proposition type and on the provider, so read it as a loose object. An empty one is {}. |
team_id | The team this sale counts towards: the agent's team when the sale was booked, which does not change when the agent later moves team. |
agent | The agent the sale is credited to, or null for a sale that is not linked to an agent — a provider delivers the sale first and the link to an agent can follow later. |
proposition | The proposition the sale was made on, or null when it is not linked to one yet. |
created_at, updated_at | When SalesDash first stored the sale, and when it last changed it. |
deleted_at | null for a live sale. Set when the sale was deleted — see below. |
Every moment is UTC with a Z. sold_at is the moment the provider reported, updated_at is SalesDash's own clock.
The agent and proposition blocks are read fresh on every call, but a change to them — a renamed agent, a proposition switched off — is not a change to the sale, so it does not bring the sale back into your next poll. Treat those blocks as a convenience, and the ids as the facts.
A sale that an admin deletes in SalesDash stays in the list, with deleted_at set, and deleting it counts as a change: the sale moves to the front of your next poll. When a sale comes back with deleted_at, delete your copy.
A sale withdrawn by its source is removed outright
When the system a provider polls withdraws a sale — a deal deleted in the CRM, an order cancelled at the source — the processor removes it from SalesDash completely, and there is no row left for this list to show. Your copy of such a sale does not get a deleted_at; it just stops coming back. If that matters for what you build, reconcile now and then: list a period without updated_from, and drop every id you hold that is no longer there.
A call returns at most limit sales. When there are more, has_more is true and next is where the list continues: pass it as cursor on the next call, with the same updated_from, updated_to and limit as before, and repeat until has_more is false.
next is an opaque token that marks the last sale you received; the next call continues strictly after it. Do not build or read it yourself. A sale that changes again while you are paging turns up again later, with its new updated_at.
The pattern SalesDash's own providers use, and the one to copy:
updated_from and follow next until has_more is false. Store every sale by id.updated_from set to the time you noted at the start of the previous run, minus a bit more than an hour, and follow next to the end.id — or delete it when deleted_at is set.The overlap is there because a sale can be written to the database a moment after the clock says the poll started. The sales you already have come back once more and are written again with the same values; that is cheap, and a lot cheaper than a sale you never see. An hour rather than a few minutes, because of one night a year: when the clocks go back in October, SalesDash's own clock repeats the hour from 02:00 to 03:00, and a sale changed in the second pass of that hour sorts before one changed in the first. An overlap longer than that hour is what guarantees you still see it; a shorter one would miss it whenever your poll interval is an hour or more. Sales changed in that hour can also show an updated_at an hour off from the real moment — once a year, and only for that hour.
Do not use the last sale's updated_at as the next run's start
It is tempting, and it works most of the time. It fails when a sale is saved with an updated_at that is a second or two earlier than the last one you received — which happens whenever two things write sales at the same time, and within the very second your last page ended. Anchoring on the moment your own run started, with the overlap above, is immune to that.
| Response | When |
|---|---|
422 on updated_from or updated_to | Not a UTC moment formatted as 2026-10-27T06:00:00Z. |
422 on updated_to | Earlier than updated_from, or past the year 9999 — leave updated_to out for no upper bound. |
422 on cursor | Not a value this endpoint handed back as next — usually a token that lost a character on the way. |
422 on limit | Not a whole number from 1 to 500. |
See the overview for the 401 and 5xx answers every endpoint shares.