Skip to content

Sales ​

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.

List sales ​

GET /integrations/api/v1/sales?updated_from=2026-10-27T06:00:00Z&limit=500
ParameterRequiredNotes
updated_fromnoOnly sales changed at or after this moment. Leave it out for the whole history.
updated_tonoOnly sales changed at or before this moment.
cursornoContinue after a page — see paging. The next value of the previous call, as-is.
limitnoSales 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.

json
{
  "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"
}
FieldMeaning
idSalesDash's own id for the sale. Stable for the sale's whole life — key your copy on it.
external_idThe id the sale has in the system it came from, if it came from a provider.
provider_idThe provider that delivered it, or null for a sale entered by hand.
sold_atWhen the sale was sold. null for a sale that is not (yet) counted as sold.
cancelled_atWhen the sale was cancelled, or null.
is_retentiontrue for a retention sale, false for a new one, null when the source did not say.
contract_value_centsThe contract value, in cents — 190100 is €1.901,00.
total_commission_centsThe commission SalesDash calculated for this sale from your commission rules, in cents. 0 when no rule applies.
contact_keyThe key that ties this sale to the contact it was made with, as the provider delivered it.
agent_id, proposition_idThe 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.
metaEverything 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_idThe team this sale counts towards: the agent's team when the sale was booked, which does not change when the agent later moves team.
agentThe 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.
propositionThe proposition the sale was made on, or null when it is not linked to one yet.
created_at, updated_atWhen SalesDash first stored the sale, and when it last changed it.
deleted_atnull 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.

Deleted sales are listed — when someone deleted them in SalesDash ​

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.

Paging ​

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.

Keep a copy current ​

The pattern SalesDash's own providers use, and the one to copy:

  1. First run: call without updated_from and follow next until has_more is false. Store every sale by id.
  2. Before each later run, note the current time in UTC. Call with 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.
  3. For every sale that comes back, update your copy by 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.

When something goes wrong ​

ResponseWhen
422 on updated_from or updated_toNot a UTC moment formatted as 2026-10-27T06:00:00Z.
422 on updated_toEarlier than updated_from, or past the year 9999 — leave updated_to out for no upper bound.
422 on cursorNot a value this endpoint handed back as next — usually a token that lost a character on the way.
422 on limitNot a whole number from 1 to 500.

See the overview for the 401 and 5xx answers every endpoint shares.