Two-way sales integration
Outbound webhooks is the CRM telling you. This is you telling the CRM — chiefly: the customer went live on our side, so close the deal on yours.
1. Get a token
Issue one from Settings → API tokens, bound to the location it belongs to. A bound token sees nothing outside that location, and rows from another one answer 404 rather than 403 — telling an outsider "that exists but is not yours" is telling them it exists.
The whole of it is in Authenticating requests: one page, and it is the same for every endpoint in this API.
2. Map your identifiers with external_ref
companies, deals, quotes and payments each carry an optional external_ref, unique inside the location. Put your own identifier there and you never have to store ours.
- Set it when you create the record (
POST /api/v1/deals). - Set it afterwards for records that predate the integration:
POST /api/v1/deals/{ulid}/external-ref. - Look records up by it:
GET /api/v1/deals?external_ref=sub_88213.
URLs use ULIDs, never sequential ids — an enumerable id would leak how much business a tenant does.
3. Reconcile
POST /api/v1/deals/reconcile
Authorization: Bearer <token>
Idempotency-Key: rec-2026-08-02-88213
Content-Type: application/json
{
"external_ref": "sub_88213",
"status": "won",
"accepted_quote_ids": [15],
"amount": "25013.94",
"paid_at": "2026-08-02",
"reference": "SPEI-8891"
}
The deal closes, the quotes you named are marked accepted (the rest are withdrawn), the payment is recorded, and the deal's timeline says the external system did it — by token name, so a seller opening the record on Monday knows why it is closed.
If the deal itself carries no external_ref, the company's is used instead — provided that company has exactly one open deal. With two, you get 404 rather than a guess about which one the money was for.
3-bis. Record a sale that never had a deal
reconcile closes a deal that already exists. When the customer bought on their own and nobody here ever opened one, POST /api/v1/sales is the call — what it decides, how it answers a renewal and what meta.matched_by means are written down in Recording a sale.
4. Idempotency is mandatory
Every mutating call in this section requires an Idempotency-Key header. One key per logical operation, kept for 24 hours.
The rules — what a replay returns, what a mismatched body returns, what is never remembered — are the same everywhere and are written down once, in Idempotency.
5. Error codes
The full catalogue — the shape of the body, what every code means and which ones are worth retrying — is in Errors. Two of them mean something extra in this section:
| Code | Meaning here |
|---|---|
404 |
Unknown external_ref, or a company with two open deals: there is no honest way to guess which one the money was for, so you get a 404 rather than a guess. |
409 |
Idempotency conflict, or the deal was already closed by a person here. Read data.status — a human decision is never overwritten in silence. |
6. The quote PDF
GET /api/v1/quotes/{ulid}/pdf returns the document with your token. The quote resource also carries pdf_url: a signed link, valid ~30 minutes, that needs no token — use it to show the document in your own UI without proxying credentials through a browser.
That link is not the customer's. The customer gets a DocumentLink with the quote's commercial validity, a branded viewer and read tracking behind it — which is how the seller finds out they opened it three times and never called back.