API
Quotes API
Create quotes (estimates) with per-line VAT, track acceptance, and convert accepted quotes into invoices.
On this page
Overview#
Quotes are priced proposals you send to a customer before invoicing. They use the same line items and VAT maths as invoices. When the customer accepts, convert the quote into an invoice in one call. Requires a plan that includes Invoicing.
| Endpoint | Purpose |
|---|---|
POST /api/v1/quotes | Create a quote |
GET /api/v1/quotes | List, filter by status or customer |
GET /api/v1/quotes/{id} | Get one quote |
PATCH /api/v1/quotes/{id} | Edit a draft or sent quote |
DELETE /api/v1/quotes/{id} | Delete a draft quote |
POST /api/v1/quotes/{id}/send | Mark it sent |
POST /api/v1/quotes/{id}/accept | Mark it accepted |
POST /api/v1/quotes/{id}/decline | Mark it declined |
POST /api/v1/quotes/{id}/convert | Create an invoice from it |
GET /api/v1/quotes/{id}/pdf | Branded quote PDF |
Wherever a path takes {id}, you can also use the quote number (e.g. QUO-000017).
The quote object#
The same object is returned by every quote endpoint and sent as data in quote.* webhooks.
{
"object": "quote",
"id": "cmg3q1a2b0001quo0001abcd",
"number": "QUO-000017",
"status": "sent",
"issueDate": "2026-09-26T22:00:00.000Z",
"expiryDate": "2026-10-26T21:59:59.000Z",
"periodStart": null,
"periodEnd": null,
"currency": "ZAR",
"taxInclusive": false,
"subtotal": 5000,
"taxAmount": 750,
"total": 5750,
"notes": "Valid for 30 days.",
"customer": {
"id": "cmg2c0s7t0003cust0001abcd",
"externalReference": "CRM-1001",
"email": "[email protected]"
},
"invoice": null,
"lineItems": [
{
"description": "Website redesign",
"details": "5 pages, mobile layout\nTwo rounds of changes",
"quantity": 1,
"unitPrice": 4500,
"amount": 4500,
"taxRate": 15,
"periodStart": null,
"periodEnd": null
},
{
"description": "Hosting setup",
"details": null,
"quantity": 1,
"unitPrice": 500,
"amount": 500,
"taxRate": 15,
"periodStart": null,
"periodEnd": null
}
],
"sentAt": "2026-09-27T08:00:00.000Z",
"acceptedAt": null,
"declinedAt": null,
"createdAt": "2026-09-27T07:55:00.000Z",
"updatedAt": "2026-09-27T08:00:00.000Z"
}| Field | Type | Description |
|---|---|---|
objectrequired | string | Always quote. |
idrequired | string | CentraPoint quote ID. |
numberrequired | string | Sequential quote number, e.g. QUO-000017. |
statusrequired | string | See Statuses. |
issueDaterequired | string | ISO 8601 UTC timestamp. |
expiryDaterequired | string | null | End of the last valid day (South African time), or null for no expiry. |
periodStart, periodEndrequired | string | null | Billing period the quote covers (YYYY-MM-DD, inclusive), or null. See Billing periods and line details. |
currencyrequired | string | ISO 4217 code. |
taxInclusiverequired | boolean | Whether line prices include tax. See VAT and totals. |
subtotal, taxAmount, totalrequired | number | Totals excluding VAT, VAT, and including VAT. |
notesrequired | string | null | Printed on the quote. |
customerrequired | object | { id, externalReference, email } |
invoicerequired | object | null | { id, number, status, externalReference } of the invoice the quote was converted into. |
lineItemsrequired | object[] | description, details (string | null), quantity, unitPrice, amount, taxRate (%), periodStart / periodEnd (YYYY-MM-DD | null). |
sentAt, acceptedAt, declinedAtrequired | string | null | When the quote was last marked sent, accepted or declined. |
createdAt, updatedAtrequired | string |
Statuses#
| status | Meaning | Can move to |
|---|---|---|
draft | Created, not yet sent. Can be edited or deleted. | sent, accepted, declined |
sent | Sent to the customer. Can still be edited. | accepted, declined, expired |
expired | Sent, and the expiry date has passed (checked daily and whenever quotes are read). | sent, accepted, declined |
accepted | The customer accepted. | declined, or convert |
declined | The customer declined. Cannot be converted. | — |
invoiced | Converted into an invoice (invoice is set). Final. | — |
Any quote except a declined or invoiced one can be converted, so you don't have to mark it accepted first.
Create a quote#
/api/v1/quotes| Field | Type | Description |
|---|---|---|
customerIdoptional | string | The customer's CentraPoint ID. Send exactly one of customerId or customerExternalReference. |
customerExternalReferenceoptional | string | Your reference of an existing customer (see the Customers API). An unknown customer returns 400. |
lineItemsrequired | object[] | 1–200 lines: description (1–1000 chars), quantity (> 0), unitPrice (≥ 0), optional taxRate (0–100, default: your organisation's tax rate from Settings → Tax). Optional details (up to 4000 chars) and periodStart / periodEnd. Same rules as invoice lines. |
currencyoptional | string | ISO 4217 code, default ZAR. |
issueDateoptional | string (YYYY-MM-DD) | Default today. |
expiryDateoptional | string (YYYY-MM-DD) | Last day the quote is valid (inclusive). Default: no expiry. Must not be before the issue date. |
periodStart, periodEndoptional | string (YYYY-MM-DD) | Billing period the quote covers, inclusive. Send both or neither; periodEnd must be on or after periodStart. Copied to the invoice on conversion. |
notesoptional | string | Up to 4000 chars. |
taxInclusiveoptional | boolean | Default false. true: line prices already include tax. |
sendoptional | boolean | Default false (draft). true creates it as sent. |
curl -X POST "https://app.centrapoint.co.za/api/v1/quotes" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY" \
-H "Idempotency-Key: 1f7c3a9e-5b2d-4e8a-b6c1-0d9e8f7a6b51" \
-H "Content-Type: application/json" \
-d '{
"customerExternalReference": "CRM-1001",
"expiryDate": "2026-10-26",
"lineItems": [
{
"description": "Website redesign",
"details": "5 pages, mobile layout\nTwo rounds of changes",
"quantity": 1,
"unitPrice": 4500
},
{
"description": "Hosting setup",
"quantity": 1,
"unitPrice": 500
}
],
"notes": "Valid for 30 days.",
"send": true
}'Returns 201 with the quote object. Quotes have no externalReference; send an Idempotency-Key so a retried request doesn't create a second quote.
List quotes#
/api/v1/quotes| Field | Type | Description |
|---|---|---|
statusoptional | string | draft, sent, accepted, declined, expired or invoiced. |
customerIdoptional | string | Only this customer's quotes. |
limit, startingAfteroptional | Keyset pagination: the response is { data, hasMore, nextCursor }, newest first. |
curl -X GET "https://app.centrapoint.co.za/api/v1/quotes?status=sent&limit=50" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY"Get a quote#
/api/v1/quotes/{id}curl -X GET "https://app.centrapoint.co.za/api/v1/quotes/cmg3q1a2b0001quo0001abcd" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY"Update a quote#
/api/v1/quotes/{id}Edits a draft or sent quote (others return 400). Takes the same fields as create except send, all optional; omitted fields keep their value. lineItems, when sent, replaces every line. expiryDate, notes, periodStart and periodEnd can be null to clear them (clear both period dates together). Emits quote.updated.
curl -X PATCH "https://app.centrapoint.co.za/api/v1/quotes/cmg3q1a2b0001quo0001abcd" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expiryDate": "2026-11-15",
"notes": "Valid until 15 November."
}'Delete a draft quote#
/api/v1/quotes/{id}Permanently deletes a draft quote and returns { "object": "quote", "id": "…", "number": "QUO-000017", "deleted": true }. Quotes that were sent can't be deleted (400); decline them instead. No webhook is sent.
Send, accept or decline#
/api/v1/quotes/{id}/send/api/v1/quotes/{id}/accept/api/v1/quotes/{id}/declineRecord the quote's progress. No body is needed. Each returns the updated quote and emits quote.sent, quote.accepted or quote.declined. A move the status table doesn't allow returns 400; sending a quote that is already sent returns it unchanged.
curl -X POST "https://app.centrapoint.co.za/api/v1/quotes/cmg3q1a2b0001quo0001abcd/accept" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY" \
-H "Idempotency-Key: 6a2e9c4f-3d1b-4a7e-8f5c-2b0d9e1a3c76"Convert to an invoice#
/api/v1/quotes/{id}/convertCreates an invoice with the quote's lines (including their details and periods), billing period, currency and tax settings (as a draft), marks the quote invoiced and links the two. Optional body { "dueDays": 14 } (0–365, default your invoice due-days setting). A pay link is created for the invoice when your plan includes payment links and a gateway accepts the currency. Send the invoice with POST /api/v1/invoices/{id}/send.
curl -X POST "https://app.centrapoint.co.za/api/v1/quotes/cmg3q1a2b0001quo0001abcd/convert" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY" \
-H "Idempotency-Key: 9d4b1e7a-2c6f-4f3a-a8e0-5c7b3d1f9e24" \
-H "Content-Type: application/json" \
-d '{
"dueDays": 14
}'Returns 201 with { "quote": <quote object>, "invoice": <invoice object> } (the invoice object). Emits quote.invoiced and invoice.created. Declined or already invoiced quotes return 400.
Download the quote PDF#
/api/v1/quotes/{id}/pdfReturns the branded quote PDF (application/pdf, as an attachment, Cache-Control: private, no-store) or 404. See Tax & PDF documents.
curl -o quote.pdf \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY" \
"https://app.centrapoint.co.za/api/v1/quotes/cmg3q1a2b0001quo0001abcd/pdf"Webhook events#
quote.created, quote.updated, quote.sent, quote.accepted, quote.declined, quote.expired and quote.invoiced are sent to your webhook endpoints with the quote object as data, whether the change came from the API or the dashboard. Moving a quote back to draft in the dashboard sends quote.updated.
Errors#
| Status | error | When |
|---|---|---|
| 400 | invalid_request | Validation failed; customer not found; invalid currency; expiry before issue date; only one of periodStart/periodEnd, or periodEnd before periodStart; total not above zero; unknown status filter; status move not allowed; editing or deleting a quote in the wrong status; converting a declined or invoiced quote. |
| 403 | plan_restricted / account_restricted | Plan lacks the API or Invoicing, or the account is read-only (writes). |
| 404 | not_found | No quote with this ID or number in your organisation. |
| 409 | idempotency_conflict | Idempotency-Key reused with a different body. |
| 401 / 429 / 500 | unauthorized / rate_limited / internal_error | See Errors |