# Two-way sales integration [Outbound webhooks](/docs/guides/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](/docs/guides/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 ```http POST /api/v1/deals/reconcile Authorization: Bearer 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](/docs/guides/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](/docs/guides/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](/docs/guides/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.