# Negocios y Cotizaciones The customer as a legal entity (D2): the thing a quote is actually addressed to, with its RFC and its fiscal details. Not to be confused with the `CompanyProfile`, which is the CRM owner's own branding. `external_ref` is the bridge to your system — set it once and every later call can find the company by your identifier instead of ours. ## List companies `GET /api/v1/companies` Lists the companies of the location the API token belongs to. ### Query parameters - `search` (string) — Free text over name, legal name and RFC. - `external_ref` (string) — Find the one carrying your identifier. - `per_page` (integer) — Rows per page. Default 50, maximum 100. See [Lists, paging and filters](#lists-paging-and-filters). ### Example request ```bash curl --request GET \ --get "https://klozzo.com/api/v1/companies?search=ADPA&external_ref=verificamex-99&per_page=50" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` ```json { "data": [ { "id": null, "ulid": null, "name": "Price Ltd", "legal_name": null, "tax_id": null, "tax_regime": null, "cfdi_use": null, "address": null, "email": "contacto@cronin.example", "phone": null, "website": null, "industry": null, "owner_id": null, "external_ref": null, "notes": null, "created_at": null, "updated_at": null }, { "id": null, "ulid": null, "name": "Hauck-Leuschke", "legal_name": null, "tax_id": null, "tax_regime": null, "cfdi_use": null, "address": null, "email": "contacto@baumbach.example", "phone": null, "website": null, "industry": null, "owner_id": null, "external_ref": null, "notes": null, "created_at": null, "updated_at": null } ] } ``` ## Create a company `POST /api/v1/companies` Creates a company in this location. A company is the fiscal counterpart of a deal: who the invoice is made out to. Create it when you are about to quote somebody who buys as a business, not for every contact — a person who buys as a person needs no company. Put your own identifier in `external_ref` and you never have to store ours. It is unique inside the location, so a retry that sends the same one gets a `422` rather than a second row. ### Body parameters - `name` (string, required) — Trading name. - `legal_name` (string) — Razón social. - `tax_id` (string) — RFC. Unique inside the location. - `tax_regime` (string) — SAT regime code. - `cfdi_use` (string) — SAT CFDI use code. - `email` (string) - `phone` (string) — The company switchboard. Free text: unlike a contact's phone this one is neither validated nor normalised, so send E.164 if you intend to dial it. - `external_ref` (string) — Your identifier for this company. Unique inside the location. ### Example request ```bash curl --request POST \ "https://klozzo.com/api/v1/companies" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --data "{ \"name\": \"ADPA\", \"legal_name\": \"ADPA, S.A. de C.V.\", \"tax_id\": \"ADP010101AB1\", \"tax_regime\": \"601\", \"cfdi_use\": \"G03\", \"email\": \"contacto@example.com\", \"phone\": \"+525555555555\", \"external_ref\": \"verificamex-99\" }" ``` ### Response `200` ```json { "data": { "id": null, "ulid": null, "name": "Price Ltd", "legal_name": null, "tax_id": null, "tax_regime": null, "cfdi_use": null, "address": null, "email": "contacto@dare.example", "phone": null, "website": null, "industry": null, "owner_id": null, "external_ref": null, "notes": null, "created_at": null, "updated_at": null } } ``` ### Response `422` — No name ```json { "message": "Falta el nombre.", "errors": { "name": [ "Falta el nombre." ] } } ``` ### Response `422` — An RFC already registered here ```json { "message": "El valor de tax id ya está en uso.", "errors": { "tax_id": [ "El valor de tax id ya está en uso." ] } } ``` ### Response `422` — An external_ref already used ```json { "message": "El valor de external ref ya está en uso.", "errors": { "external_ref": [ "El valor de external ref ya está en uso." ] } } ``` ## Fetch a company `GET /api/v1/companies/{ulid}` Returns one company by its ULID, with everything on file plus `contacts_count` and `deals_count`, which is usually what you actually wanted: whether this company is worth a call. A company from another location answers `404`, never `403` — telling an outsider "that exists but is not yours" is telling them it exists. ### Path parameters - `ulid` (string, required) — Company ULID (a numeric id still resolves). ### Example request ```bash curl --request GET \ --get "https://klozzo.com/api/v1/companies/01JZ8Z0ZP0000000000000000" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` ```json { "data": { "id": null, "ulid": null, "name": "Price Ltd", "legal_name": null, "tax_id": null, "tax_regime": null, "cfdi_use": null, "address": null, "email": "contacto@hauck.example", "phone": null, "website": null, "industry": null, "owner_id": null, "external_ref": null, "notes": null, "created_at": null, "updated_at": null } } ``` ## Update a company `PUT /api/v1/companies/{ulid}` Updates a company by its ULID, changing only the fields you send. Fields you leave out are left alone, so this is safe to call from a sync that only knows about the tax data. Use it to fill in fiscal details a seller could not collect at the time — an RFC that arrives with the purchase order, a CFDI use the accountant corrects. Changing `name` here does not rename anything else: deals and quotes already issued keep the name they were issued with. ### Path parameters - `ulid` (string, required) — Company ULID (a numeric id still resolves). ### Example request ```bash curl --request PUT \ "https://klozzo.com/api/v1/companies/01JZ8Z0ZP0000000000000000" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` ```json { "data": { "id": null, "ulid": null, "name": "Price Ltd", "legal_name": null, "tax_id": null, "tax_regime": null, "cfdi_use": null, "address": null, "email": "contacto@leuschke.example", "phone": null, "website": null, "industry": null, "owner_id": null, "external_ref": null, "notes": null, "created_at": null, "updated_at": null } } ``` ## List deals `GET /api/v1/deals` Lists the deals of this location, newest first. Use it to reconcile: ask for `?external_ref=` to find the one carrying your identifier, or for `?status=open` to see what is still in play before you close something on your side. **What you see depends on the token.** A token belonging to somebody who manages the location sees every deal; a seller's token sees only their own. That is not a filter you can turn off, so a partial list is not a bug — it is whose token you are using. ### Query parameters - `status` (string) — One of `open`, `won`, `lost`. - `company_id` (integer) — Only this company's deals. - `external_ref` (string) — Find the one carrying your identifier. - `per_page` (integer) — Rows per page. Default 50, maximum 100. See [Lists, paging and filters](#lists-paging-and-filters). ### Example request ```bash curl --request GET \ --get "https://klozzo.com/api/v1/deals?status=open&company_id=12&external_ref=sub_88213&per_page=50" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` ```json { "data": [ { "id": 1, "ulid": "01KYDKV7N1RMWVK8H79M9H20V1", "title": "ADPA — Paquete 1,100 firmas", "description": null, "status": "open", "status_label": "Abierto", "amount": { "amount": 250140000, "currency": "MXN", "decimal": "25014.0000", "formatted": "$25,014.00" }, "paid": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "balance": { "amount": 250140000, "currency": "MXN", "decimal": "25014.0000", "formatted": "$25,014.00" }, "weighted_amount": { "amount": 100056000, "currency": "MXN", "decimal": "10005.6000", "formatted": "$10,005.60" }, "currency": "MXN", "pipeline_id": 1, "pipeline_stage_id": 2, "stage": { "id": 2, "name": "Cotización enviada", "probability": 40, "is_won": false, "is_lost": false }, "company_id": 1, "primary_contact_id": null, "owner_id": 26, "expected_close_date": null, "closed_at": null, "lost_reason": null, "source": null, "attribution": null, "external_ref": null, "created_at": "2026-07-25T03:58:03+00:00", "updated_at": "2026-07-25T03:58:03+00:00" }, { "id": 1, "ulid": "01KYDKV7N1RMWVK8H79M9H20V1", "title": "ADPA — Paquete 1,100 firmas", "description": null, "status": "open", "status_label": "Abierto", "amount": { "amount": 250140000, "currency": "MXN", "decimal": "25014.0000", "formatted": "$25,014.00" }, "paid": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "balance": { "amount": 250140000, "currency": "MXN", "decimal": "25014.0000", "formatted": "$25,014.00" }, "weighted_amount": { "amount": 100056000, "currency": "MXN", "decimal": "10005.6000", "formatted": "$10,005.60" }, "currency": "MXN", "pipeline_id": 1, "pipeline_stage_id": 2, "stage": { "id": 2, "name": "Cotización enviada", "probability": 40, "is_won": false, "is_lost": false }, "company_id": 1, "primary_contact_id": null, "owner_id": 26, "expected_close_date": null, "closed_at": null, "lost_reason": null, "source": null, "attribution": null, "external_ref": null, "created_at": "2026-07-25T03:58:03+00:00", "updated_at": "2026-07-25T03:58:03+00:00" } ] } ``` ## Create a deal `POST /api/v1/deals` Creates a deal — an opportunity — in the location's default pipeline unless another is named. The attribution of its contact is frozen onto it at this moment and never recalculated (D17). ### Body parameters - `title` (string, required) — What is being sold. - `description` (string) — Must not be greater than 5000 characters. - `company_id` (integer) — The customer company. - `primary_contact_id` (integer) — The person on the other end. - `owner_id` (integer) — The agent responsible. Defaults to the token's user. - `pipeline_id` (integer) — Defaults to the location's default pipeline. - `pipeline_stage_id` (string) - `expected_close_date` (string) - `source` (string) — Where it came from. - `external_ref` (string) — Your identifier for this deal. Unique inside the location. ### Example request ```bash curl --request POST \ "https://klozzo.com/api/v1/deals" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --data "{ \"title\": \"ADPA — Paquete 1,100 firmas\", \"description\": \"Et animi quos velit et fugiat.\", \"company_id\": 12, \"primary_contact_id\": 340, \"owner_id\": 4, \"pipeline_id\": 1, \"expected_close_date\": \"2026-08-30\", \"source\": \"verificamex\", \"external_ref\": \"sub_88213\" }" ``` ### Response `200` ```json { "data": { "id": 1, "ulid": "01KYDKV7N1RMWVK8H79M9H20V1", "title": "ADPA — Paquete 1,100 firmas", "description": null, "status": "open", "status_label": "Abierto", "amount": { "amount": 250140000, "currency": "MXN", "decimal": "25014.0000", "formatted": "$25,014.00" }, "paid": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "balance": { "amount": 250140000, "currency": "MXN", "decimal": "25014.0000", "formatted": "$25,014.00" }, "weighted_amount": { "amount": 100056000, "currency": "MXN", "decimal": "10005.6000", "formatted": "$10,005.60" }, "currency": "MXN", "pipeline_id": 1, "pipeline_stage_id": 2, "stage": { "id": 2, "name": "Cotización enviada", "probability": 40, "is_won": false, "is_lost": false }, "company_id": 1, "primary_contact_id": null, "owner_id": 26, "expected_close_date": null, "closed_at": null, "lost_reason": null, "source": null, "attribution": null, "external_ref": null, "created_at": "2026-07-25T03:58:03+00:00", "updated_at": "2026-07-25T03:58:03+00:00" } } ``` ## Reconcile a deal `POST /api/v1/deals/reconcile` Finds the deal by your identifier and closes it. If you did not put an `external_ref` on the deal, the company's is used instead — provided that company has exactly one open deal, because guessing which of three deals a payment belongs to is not a guess anybody should make. ### Body parameters - `external_ref` (string, required) — Your identifier for the deal, or for its company. - `status` (string) — One of `won` or `lost`. Defaults to `won`. - `lost_reason` (string) — Required when `status` is `lost`. - `accepted_quote_ids` (integer[]) — Which live quotes the customer actually took (D26). Omitted means none of them did, which is a valid answer. - `amount` (string) — Record a payment of this size at the same time. - `paid_at` (string) — When the money arrived. Defaults to now. - `reference` (string) — Bank or charge reference. - `payment_external_ref` (string) — Your identifier for the payment. ### Example request ```bash curl --request POST \ "https://klozzo.com/api/v1/deals/reconcile" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Idempotency-Key: required A unique value per operation. A retry with the same key returns the first answer instead of doing the work again. Example: rec-2026-08-02-88213" \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --data "{ \"external_ref\": \"sub_88213\", \"status\": \"won\", \"lost_reason\": \"Se fue con la competencia\", \"accepted_quote_ids\": [ 15 ], \"amount\": \"25013.94\", \"paid_at\": \"2026-08-02\", \"reference\": \"SPEI-8891\", \"payment_external_ref\": \"charge_9912\" }" ``` ### Response `200` — Reconciled ```json { "data": { "status": "won" }, "meta": { "reconciled": true, "payment_recorded": true } } ``` ### Response `404` — Nothing to reconcile ```json { "message": "No hay ningún negocio con ese external_ref." } ``` ### Response `409` — Already decided here ```json { "message": "El negocio ya está cerrado…", "data": { "status": "won" } } ``` ## Link a deal to your identifier `POST /api/v1/deals/{deal_ulid}/external-ref` Attaches your own identifier to a deal that already exists — for the ones that predate the integration. One deal, one `external_ref`, unique inside the location. ### Path parameters - `deal_ulid` (string, required) — Deal ULID. ### Body parameters - `external_ref` (string, required) — Your identifier. ### Example request ```bash curl --request POST \ "https://klozzo.com/api/v1/deals/01JZ8Z0ZP0000000000000000/external-ref" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Idempotency-Key: required A unique value per operation. Example: link-88213" \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --data "{ \"external_ref\": \"sub_88213\" }" ``` ### Response `200` ```json { "data": { "id": 1, "ulid": "01KYDKV7N1RMWVK8H79M9H20V1", "title": "ADPA — Paquete 1,100 firmas", "description": null, "status": "open", "status_label": "Abierto", "amount": { "amount": 250140000, "currency": "MXN", "decimal": "25014.0000", "formatted": "$25,014.00" }, "paid": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "balance": { "amount": 250140000, "currency": "MXN", "decimal": "25014.0000", "formatted": "$25,014.00" }, "weighted_amount": { "amount": 100056000, "currency": "MXN", "decimal": "10005.6000", "formatted": "$10,005.60" }, "currency": "MXN", "pipeline_id": 1, "pipeline_stage_id": 2, "stage": { "id": 2, "name": "Cotización enviada", "probability": 40, "is_won": false, "is_lost": false }, "company_id": 1, "primary_contact_id": null, "owner_id": 26, "expected_close_date": null, "closed_at": null, "lost_reason": null, "source": null, "attribution": null, "external_ref": null, "created_at": "2026-07-25T03:58:03+00:00", "updated_at": "2026-07-25T03:58:03+00:00" } } ``` ## Fetch a deal `GET /api/v1/deals/{ulid}` Returns one deal by its ULID, with its stage and its company. Read `status` before you act on it: a deal a person here already closed is never flipped by the API, and this is where you find out that happened. ### Path parameters - `ulid` (string, required) — Deal ULID (a numeric id still resolves). ### Example request ```bash curl --request GET \ --get "https://klozzo.com/api/v1/deals/01JZ8Z0ZP0000000000000000" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` ```json { "data": { "id": 1, "ulid": "01KYDKV7N1RMWVK8H79M9H20V1", "title": "ADPA — Paquete 1,100 firmas", "description": null, "status": "open", "status_label": "Abierto", "amount": { "amount": 250140000, "currency": "MXN", "decimal": "25014.0000", "formatted": "$25,014.00" }, "paid": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "balance": { "amount": 250140000, "currency": "MXN", "decimal": "25014.0000", "formatted": "$25,014.00" }, "weighted_amount": { "amount": 100056000, "currency": "MXN", "decimal": "10005.6000", "formatted": "$10,005.60" }, "currency": "MXN", "pipeline_id": 1, "pipeline_stage_id": 2, "stage": { "id": 2, "name": "Cotización enviada", "probability": 40, "is_won": false, "is_lost": false }, "company_id": 1, "primary_contact_id": null, "owner_id": 26, "expected_close_date": null, "closed_at": null, "lost_reason": null, "source": null, "attribution": null, "external_ref": null, "created_at": "2026-07-25T03:58:03+00:00", "updated_at": "2026-07-25T03:58:03+00:00" } } ``` ## List quotes `GET /api/v1/quotes` Lists the quotes issued in this location, newest first. Filter by `deal_id` to see the options offered on one opportunity, or by `folio` to look up the code a customer read out on the phone — with or without its prefix and dash, because nobody dictates punctuation. A draft has no folio yet: the code is assigned when the quote is shared, which is also the moment its contents are frozen. ### Query parameters - `deal_id` (integer) — Only this deal's quotes. - `status` (string) — One of `draft`, `shared`, `viewed`, `accepted`, `rejected`, `withdrawn`, `expired`, `superseded`. - `folio` (string) — The dictated code, with or without prefix or dash. - `per_page` (integer) — Rows per page. Default 50, maximum 100. See [Lists, paging and filters](#lists-paging-and-filters). ### Example request ```bash curl --request GET \ --get "https://klozzo.com/api/v1/quotes?deal_id=7&status=shared&folio=COT-7K3M9&per_page=50" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` ```json { "data": [ { "id": 1, "ulid": "01KYDKV7P4RVYJZZ6JNAJ521YR", "deal_id": 15, "company_id": null, "contact_id": 6295, "owner_id": 26, "quote_template_id": null, "folio": null, "reference": "Borrador", "version": 1, "title": "Patito", "intro": null, "status": "draft", "status_label": "Borrador", "is_option": false, "currency": "MXN", "subtotal": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "discount_total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "tax_total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "valid_until": "2026-08-09", "shared_at": null, "decided_at": null, "snapshot_hash": null, "external_ref": null, "pdf_url": "http://klozzo.com/api/v1/quotes/01KYDKV7P4RVYJZZ6JNAJ521YR/pdf/signed?893456000&signature=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "created_at": "2026-07-25T18:11:14+00:00", "updated_at": "2026-07-25T18:11:14+00:00" }, { "id": 1, "ulid": "01KYDKV7P4RVYJZZ6JNAJ521YR", "deal_id": 15, "company_id": null, "contact_id": 6295, "owner_id": 26, "quote_template_id": null, "folio": null, "reference": "Borrador", "version": 1, "title": "Patito", "intro": null, "status": "draft", "status_label": "Borrador", "is_option": false, "currency": "MXN", "subtotal": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "discount_total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "tax_total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "valid_until": "2026-08-09", "shared_at": null, "decided_at": null, "snapshot_hash": null, "external_ref": null, "pdf_url": "http://klozzo.com/api/v1/quotes/01KYDKV7P4RVYJZZ6JNAJ521YR/pdf/signed?893456000&signature=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "created_at": "2026-07-25T18:11:14+00:00", "updated_at": "2026-07-25T18:11:14+00:00" } ] } ``` ## Create a quote `POST /api/v1/quotes` Creates a quote as a **draft** on an existing deal, with its lines. A draft burns no folio and is not visible to the customer: sharing it is a separate, deliberate act that freezes the document (D24). ### Body parameters - `deal_id` (integer, required) — The deal this offer belongs to. - `title` (string, required) - `intro` (string) — Opening paragraph, may carry `{{quote.*}}` variables. - `valid_until` (string) — Commercial validity. Defaults to the location's setting. - `is_option` (boolean) — Mark it when the customer will choose between several (D25). - `company_id` (integer) - `contact_id` (integer) - `quote_template_id` (integer) — The design to print with. - `items` (object[], required) — The lines. Totals are derived from them. - `name` (string, required) - `quantity` (string, required) - `unit_price` (string, required) — Up to four decimals. - `unit_label` (string) - `tax_rate_id` (integer) — A tax rate of this location. - `discount_percent` (string) - `is_informational` (boolean) — Shows a price and adds nothing. - `attributes` (object) — The service's own columns, e.g. `{"tokens_por_validacion": 3}`. ### Example request ```bash curl --request POST \ "https://klozzo.com/api/v1/quotes" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --data "{ \"deal_id\": 7, \"title\": \"Firmado Electrónico\", \"valid_until\": \"2026-08-14\", \"is_option\": false, \"company_id\": 12, \"contact_id\": 340, \"quote_template_id\": 3, \"items\": [ { \"name\": \"Costo de las firmas por volumen\", \"quantity\": \"1100\", \"unit_price\": \"19.6034\", \"unit_label\": \"firma\", \"tax_rate_id\": 1, \"discount_percent\": \"0\", \"is_informational\": false } ] }" ``` ### Response `200` ```json { "data": { "id": 1, "ulid": "01KYDKV7P4RVYJZZ6JNAJ521YR", "deal_id": 15, "company_id": null, "contact_id": 6295, "owner_id": 26, "quote_template_id": null, "folio": null, "reference": "Borrador", "version": 1, "title": "Patito", "intro": null, "status": "draft", "status_label": "Borrador", "is_option": false, "currency": "MXN", "subtotal": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "discount_total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "tax_total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "valid_until": "2026-08-09", "shared_at": null, "decided_at": null, "snapshot_hash": null, "external_ref": null, "pdf_url": "http://klozzo.com/api/v1/quotes/01KYDKV7P4RVYJZZ6JNAJ521YR/pdf/signed?893456000&signature=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "created_at": "2026-07-25T18:11:14+00:00", "updated_at": "2026-07-25T18:11:14+00:00" } } ``` ## Fetch a quote `GET /api/v1/quotes/{ulid}` Returns one quote by its ULID, with its line items and its totals. Money always comes back as an object — `amount` is an integer at scale 10⁴, `decimal` is the exact string, `formatted` is for display. Never parse `formatted`. `pdf_url` is a short-lived signed link you can put straight into your own interface without proxying a token through a browser. It is **not** the customer's link: that one carries the commercial validity and the read tracking behind it. ### Path parameters - `ulid` (string, required) — Quote ULID (a numeric id still resolves). ### Example request ```bash curl --request GET \ --get "https://klozzo.com/api/v1/quotes/01JZ8Z0ZP0000000000000000" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` ```json { "data": { "id": 1, "ulid": "01KYDKV7P4RVYJZZ6JNAJ521YR", "deal_id": 15, "company_id": null, "contact_id": 6295, "owner_id": 26, "quote_template_id": null, "folio": null, "reference": "Borrador", "version": 1, "title": "Patito", "intro": null, "status": "draft", "status_label": "Borrador", "is_option": false, "currency": "MXN", "subtotal": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "discount_total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "tax_total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "total": { "amount": 0, "currency": "MXN", "decimal": "0.0000", "formatted": "$0.00" }, "valid_until": "2026-08-09", "shared_at": null, "decided_at": null, "snapshot_hash": null, "external_ref": null, "pdf_url": "http://klozzo.com/api/v1/quotes/01KYDKV7P4RVYJZZ6JNAJ521YR/pdf/signed?893456000&signature=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2", "created_at": "2026-07-25T18:11:14+00:00", "updated_at": "2026-07-25T18:11:14+00:00" } } ``` ## Download the quote PDF `GET /api/v1/quotes/{quote_ulid}/pdf` Returns the PDF of one quote, rendered from the frozen snapshot (D8), so the same quote produces the same bytes in a year's time. Reachable two ways: with an API token, or through the short-lived signed `pdf_url` the quote resource hands out — which is what lets your own UI show the document without proxying a token through somebody's browser. This is **not** the customer's link. That one is a `DocumentLink` with the commercial validity, the branded viewer and read tracking behind it (D13). ### Path parameters - `quote_ulid` (string, required) — Quote ULID. ### Example request ```bash curl --request GET \ --get "https://klozzo.com/api/v1/quotes/01JZ8Z0ZP0000000000000000/pdf" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` — PDF ```json "%PDF-1.7 …" ``` ## Record a sale `POST /api/v1/sales` Records a sale for a purchase that happened entirely on your side, with nobody here opening a deal for it — a self-service checkout, a renewal that charged itself. Send the money and the customer; the CRM works out what it means and tells you which rule fired in `meta.matched_by`. It may attach the sale to an open deal the customer already had, or open one **already won and with no owner** when there is none — because nobody here attended this customer, and giving the commission to whoever happens to be listed would be worse than leaving it unclaimed. When that happens `meta.claimable` is `true` and a seller claims it from the CRM. Two open deals for the same customer never resolve to a guess: a new deal is opened and the ambiguity is left for a person. Use `POST /api/v1/deals/reconcile` instead when the deal already exists and you know which one, and `POST /api/v1/payments` for a further instalment of a sale already recorded here. ### Body parameters - `external_ref` (string, required) — Your identifier for THIS sale. Recording it twice is impossible. - `amount` (string, required) — What was paid. Up to four decimals, separators tolerated. - `currency` (string) — ISO 4217. Defaults to the location's. - `paid_at` (string) — When the money arrived. Defaults to now. - `method` (string) — One of `transferencia`, `efectivo`, `tarjeta`, `otro`. Defaults to `tarjeta`. - `reference` (string) — Bank or charge reference. - `title` (string) — Title for the deal if one has to be created. - `deal_external_ref` (string) — Point the sale at one specific deal. Answers 404 if that deal is not here. - `customer` (object) — Who bought. Required unless `deal_external_ref` identifies the deal. - `email` (string) - `phone` (string) - `phone_country` (string) — Must not be greater than 2 characters. - `first_name` (string) - `last_name` (string) - `company_name` (string) - `company_external_ref` (string) — Your identifier for the company. - `plan` (object) — How long what was bought lasts. Omit it for a one-off purchase. - `name` (string) - `product_sku` (string) — Matches a product in the catalogue. - `external_ref` (string) — Your identifier for the term. Sending it again EXTENDS the same term instead of opening a second one — that is how a renewal is told from a new sale. - `starts_at` (string) — Defaults to today. - `ends_at` (string) — Leave both this and `term_months` out for a purchase that never expires. - `term_months` (integer) — Used to derive `ends_at` when you do not send one. - `auto_renew` (boolean) — Whether your system will renew it on its own. ### Example request ```bash curl --request POST \ "https://klozzo.com/api/v1/sales" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Idempotency-Key: required A unique value per operation. A retry with the same key returns the first answer instead of doing the work again. Example: sale-2026-08-23-88213" \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --data "{ \"external_ref\": \"sale_88213\", \"amount\": \"1499.00\", \"currency\": \"MXN\", \"paid_at\": \"2026-08-23T10:00:00Z\", \"method\": \"tarjeta\", \"reference\": \"ch_9912\", \"title\": \"Plan Pro anual\", \"deal_external_ref\": \"sub_44\", \"customer\": { \"email\": \"ada@example.com\", \"phone\": \"+52 55 1234 5678\", \"phone_country\": \"kh\", \"first_name\": \"Ada\", \"last_name\": \"Lovelace\", \"company_name\": \"ADPA\", \"company_external_ref\": \"verificamex-99\" }, \"plan\": { \"name\": \"Plan Pro\", \"product_sku\": \"PLAN-PRO\", \"external_ref\": \"sub_88213\", \"starts_at\": \"2026-08-23\", \"ends_at\": \"2027-08-23\", \"term_months\": 12, \"auto_renew\": true } }" ``` ### Response `200` — Already recorded ```json { "meta": { "created": false, "matched_by": "payment_external_ref" } } ``` ### Response `201` — Nobody had attended this customer ```json { "data": { "deal": { "status": "won", "owner_id": null } }, "meta": { "created": true, "deal_created": true, "matched_by": "created", "claimable": true } } ``` ### Response `404` — You named a deal that is not here ```json { "message": "No hay ningún negocio con el external_ref «sub_44»." } ``` ## List payments `GET /api/v1/payments` Lists the payments recorded in this location, most recent first. Use it to check what your side has already reported before reporting it again — `?external_ref=` finds the one carrying your charge identifier, which is the cheapest way to answer "did that webhook land?". ### Query parameters - `deal_id` (integer) — Only this deal's payments. - `external_ref` (string) — Find the one carrying your identifier. - `per_page` (integer) — Rows per page. Default 50, maximum 100. See [Lists, paging and filters](#lists-paging-and-filters). ### Example request ```bash curl --request GET \ --get "https://klozzo.com/api/v1/payments?deal_id=7&external_ref=spei-8891&per_page=50" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" ``` ### Response `200` ```json { "data": [ { "id": null, "deal_id": null, "quote_id": null, "subscription_id": null, "amount": { "amount": 100000000, "currency": "MXN", "decimal": "10000.0000", "formatted": "$10,000.00" }, "currency": "MXN", "paid_at": "2026-08-17T02:19:50+00:00", "method": "transferencia", "method_label": "Transferencia", "origin": null, "reference": "SPEI-3724", "note": null, "recorded_by": null, "external_ref": null, "created_at": null }, { "id": null, "deal_id": null, "quote_id": null, "subscription_id": null, "amount": { "amount": 100000000, "currency": "MXN", "decimal": "10000.0000", "formatted": "$10,000.00" }, "currency": "MXN", "paid_at": "2026-08-15T02:19:50+00:00", "method": "transferencia", "method_label": "Transferencia", "origin": null, "reference": "SPEI-0432", "note": null, "recorded_by": null, "external_ref": null, "created_at": null } ] } ``` ## Record a payment `POST /api/v1/payments` Records a payment against a deal that already exists. It does not close the deal and it does not accept a quote — use `POST /api/v1/deals/reconcile` when the payment is what closes the sale, or `POST /api/v1/sales` when there was never a deal to begin with. Instalments are several calls against the same `deal_id`; give each one its own `external_ref` so a retry of the second never looks like the first. ### Body parameters - `deal_id` (integer, required) — The deal being paid. - `quote_id` (integer) — The quote it settles, when there is one. - `amount` (string, required) — Up to four decimals, separators tolerated. - `paid_at` (string, required) — When the money arrived. - `method` (string) — One of `transferencia`, `efectivo`, `tarjeta`, `otro`. - `reference` (string) — Bank reference. - `note` (string) - `external_ref` (string) — Your identifier for this payment. ### Example request ```bash curl --request POST \ "https://klozzo.com/api/v1/payments" \ --header "Authorization: Bearer {YOUR_API_TOKEN}" \ --header "Content-Type: application/json" \ --header "Accept: application/json" \ --data "{ \"deal_id\": 7, \"quote_id\": 15, \"amount\": \"25013.94\", \"paid_at\": \"2026-08-02\", \"method\": \"transferencia\", \"reference\": \"SPEI-8891\", \"note\": \"Primer parcialidad\", \"external_ref\": \"charge_9912\" }" ``` ### Response `200` ```json { "data": { "id": null, "deal_id": null, "quote_id": null, "subscription_id": null, "amount": { "amount": 100000000, "currency": "MXN", "decimal": "10000.0000", "formatted": "$10,000.00" }, "currency": "MXN", "paid_at": "2026-08-12T02:19:50+00:00", "method": "transferencia", "method_label": "Transferencia", "origin": null, "reference": "SPEI-0564", "note": null, "recorded_by": null, "external_ref": null, "created_at": null } } ``` ### Response `422` — A deal from another location, or none ```json { "message": "Deal id no es válido.", "errors": { "deal_id": [ "Deal id no es válido." ] } } ``` ### Response `422` — An amount that is not a number ```json { "message": "Amount no es un importe válido.", "errors": { "amount": [ "Amount no es un importe válido." ] } } ```