QuoteBill API: create invoices from Make, Zapier, n8n and Notion workflows
Create invoices and quotations in your QuoteBill account from a spreadsheet row, Make, Zapier, n8n or a script, with a personal API key. Five endpoints, curl examples and limits.
The QuoteBill API lets Make, Zapier, n8n, a Notion-based workflow or your own script create a quotation, invoice or other document in your QuoteBill account. A new row in a spreadsheet becomes a draft you open in the editor, check, and send.
It is a small API on purpose: it creates documents and reads your own. It cannot send anything, sign anything or change your account. It is for QuoteBill members; you make and revoke keys in Settings.
Before you start
- Sign in to QuoteBill and confirm your e-mail address, if you have not yet.
- Open Company settings, find API keys, give the key a name (for example "Make: Google Sheets"), choose how long it is valid (30, 90 or 365 days) and press Create key.
- Copy the key from the window that opens. It is shown once and QuoteBill does not keep it, so a lost key is replaced, not recovered.
- Put it in the credential store of the tool that will use it, and send a request to /v1/ping to check that it works.
Every address below starts with https://wgpfbcfiontxqusseoyv.supabase.co/functions/v1/api.
curl https://wgpfbcfiontxqusseoyv.supabase.co/functions/v1/api/v1/ping \
-H "Authorization: Bearer $QUOTEBILL_API_KEY"
{"ok":true}Authentication
Send the key in the Authorization header of every request: Authorization: Bearer qbk_ followed by 32 letters and digits. It is accepted nowhere else: the API never reads a key from a web address or from a body, and an address that carries a key should be treated as leaked.
- A key belongs to the member who made it and can only reach that member's documents.
- A key has two scopes: documents:create (make documents) and documents:read (list and read them). Both are given to every new key.
- A key expires after the 30, 90 or 365 days you chose, and you can revoke it at any time; it stops working on the very next request.
- You can have five active keys at a time, so each workflow can have its own.
- A key that is missing, wrong, revoked or expired gets the same answer: 401 invalid_api_key.
The API sends no CORS headers: a web page cannot call it from a visitor's browser, which is what keeps a key out of web pages. Call it from a server, a script or an automation tool.
The five endpoints
| Endpoint | What it does | Scope |
|---|---|---|
GET /v1/ping | Checks that the key works. Answers {"ok":true}. | any key |
GET /v1/templates | Lists the kinds of document and the templates you can ask for. | any key |
POST /v1/documents | Makes a document: a draft saved in your cloud. | documents:create |
GET /v1/documents | Lists your documents, newest first: kind, status, number, customer and dates only. | documents:read |
GET /v1/documents/{id} | Reads one of your documents: customer, items, notes and totals. | documents:read |
Every answer is JSON, is never cached, and has no CORS headers. Send a body only to POST /v1/documents, as JSON with Content-Type: application/json, at most 256 KB.
Make a document: POST /v1/documents
The document is made the way the editor makes one, with your company details as the sender, and saved as a draft in your cloud. It counts against your cloud document limit exactly as a document you save yourself does. Open editor_url to check and send it.
curl -X POST https://wgpfbcfiontxqusseoyv.supabase.co/functions/v1/api/v1/documents \
-H "Authorization: Bearer $QUOTEBILL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sheet-row-17" \
-d '{"kind":"invoice","country":"KR","issue_date":"2026-10-04","due_date":"2026-11-03","customer":{"name":"Acme Ltd","email":"billing@acme.example","address":"12 Harbour Road, Rotterdam","tax_id":"NL123456789B01"},"items":[{"description":"Website design","quantity":1,"unit_price":1500000},{"description":"Hosting (12 months)","quantity":12,"unit_price":10000}],"notes":"Thank you for your business."}'{
"id": "doc-7c0b8a4e-3f1d-4a52-9d6e-0b2f5c1e8a77",
"editor_url": "https://quotebill.com/en/editor/doc-7c0b8a4e-3f1d-4a52-9d6e-0b2f5c1e8a77",
"number": "INVOICE-20261004-1-9F3A1C",
"totals": {
"currency": "KRW",
"subtotal": 1620000,
"tax_name": "VAT",
"tax_rate": 10,
"tax_amount": 162000,
"total": 1782000,
"tax_status": "calculated"
}
}The fields of the request. Only kind, customer.name and items are required; leave out what you do not need, or send it empty (an empty cell of a sheet counts as left out).
| Field | Meaning |
|---|---|
kind | quotation, invoice, proforma, receipt, creditNote, purchaseOrder, deliveryNote, workOrder or commercialInvoice. A statement of account is not made through the API. |
template | A template slug from GET /v1/templates, of the same kind. It chooses the layout, the line columns and the printed title. |
language | The language the document is written in: one of the 33 QuoteBill is written in, such as en, ko, ja or de. Default: your company setting, else en. |
country | Two capital letters, such as KR or US. The tax follows the country. Default: your company setting, else US. |
currency | Three capital letters, such as USD or KRW. Default: the country's own currency. Amounts are rounded to the currency's decimals. |
issue_date | YYYY-MM-DD. Default: today (UTC). |
due_date | YYYY-MM-DD, for an invoice or commercial invoice. Default: 30 days after the issue date. |
valid_until | YYYY-MM-DD, for a quotation or proforma invoice. Default: 30 days after the issue date. |
document_number | Up to 100 characters. Default: a number made as your numbering setting says, automatic or in sequence. A number another of your cloud documents already has is refused (409 document_number_taken). |
tax_rate | A percentage from 0 to 100 for the whole document. Default: your company setting, else the country's standard rate. |
customer | An object: name (required, up to 200 characters), email, address (up to 500) and tax_id (up to 60). |
items | One to 200 objects: description (required, up to 500 characters), quantity (a number above 0), unit_price (a number from 0) and, optionally, tax_rate. A QuoteBill document has one tax rate, so an item's tax_rate must equal the document's and every other item's. |
notes | Up to 2,000 characters, printed on the document. |
Numbers must be JSON numbers (10, 9.99), not text ("10"). Texts must be one line, except notes and address. A field the API does not know is refused, so a typo is found at once rather than ignored.
The answer has the document's id, the address that opens it in the editor, its number and its totals. totals is null for a document whose lines carry no prices (a delivery note of quantities). Where a country has no single national rate (the United States is one), totals says tax_status "rate_needed" and gives no tax or total until you send tax_rate; the editor asks for the rate in the same way.
Add an Idempotency-Key header (1 to 255 letters, digits, dots, dashes, underscores or colons) and a retry of the same request within 24 hours returns the same document with status 200 instead of making another. Use something that identifies the source, such as the id of the row. The same key with a different request is refused (409 idempotency_key_reused).
List and read: GET /v1/documents
Lists your own documents, whoever made them, newest first. Parameters: kind, status (for example draft, sent, paid), limit (1 to 100, default 25) and cursor. When there are more, the answer has a next_cursor; send it back as cursor to get the next page.
curl "https://wgpfbcfiontxqusseoyv.supabase.co/functions/v1/api/v1/documents?kind=invoice&limit=10" \
-H "Authorization: Bearer $QUOTEBILL_API_KEY"curl https://wgpfbcfiontxqusseoyv.supabase.co/functions/v1/api/v1/documents/doc-7c0b8a4e-3f1d-4a52-9d6e-0b2f5c1e8a77 \
-H "Authorization: Bearer $QUOTEBILL_API_KEY"A document that is not yours, or does not exist, is a 404 not_found, the same for both. Reading one returns the customer, the items, the notes and the totals; the sender's details, logo and stamp are not part of it. There is no way to change or delete a document through the API: do that in the editor.
Google Sheets row to invoice, in Make, Zapier and n8n
The recipe is the same in every tool: when a row is added to a sheet, send one HTTP request to POST /v1/documents. Only the generic request features of each tool are used. Steps and menu names in those products may change; follow the product's own help for where a setting lives.
- Make one row per invoice, with columns such as Customer, Customer e-mail, Item, Quantity, Unit price and an id you can use as the Idempotency-Key.
- Trigger: a new row in the sheet.
- Action: an HTTP request, as below.
- Keep the answer's editor_url wherever you keep the results, for example in the same row.
{
"kind": "invoice",
"customer": { "name": "<Customer>", "email": "<Customer e-mail>" },
"items": [ { "description": "<Item>", "quantity": <Quantity>, "unit_price": <Unit price> } ]
}Quantity and Unit price go in without quotation marks, and must reach the request as numbers: convert a cell that arrives as text with the tool's number conversion first. Several rows for one invoice can be gathered into one items list by your tool, or sent as one invoice per row.
- Make: the HTTP module's "Make a request". URL: the address of POST /v1/documents. Method: POST. Headers: Authorization with the value Bearer and your key, and Content-Type application/json. Body type: Raw, content type JSON, request content: the body above.
- Zapier: "Webhooks by Zapier", the Custom Request action. Method: POST. URL: the same. Data: the body above. Headers: Authorization (Bearer and your key) and Content-Type (application/json), and Idempotency-Key with the row's id.
- n8n: the HTTP Request node. Method: POST. URL: the same. Authentication: a Header Auth credential named Authorization with the value Bearer and your key, so the key lives in n8n's credentials and not in the workflow. Send Body on, body content type JSON, body as above.
A key pasted into a header like this sits in plain sight: the header value of a scenario or a Zap is visible to everyone who can edit or share it, and it can end up in exports and in run history. Use a key made for that one workflow, never share or export a scenario or Zap that holds a key, and where the tool has its own connection or credential feature for API keys, use that instead of typing the key into a header. If a workflow that holds a key has been shared or exported, revoke the key and make a new one.
Notion workflows
QuoteBill does not connect to Notion by itself. What works is the same recipe with Notion as the source: Make and n8n can read a Notion database, and either can then send the HTTP request above.
- Keep the billing rows in a Notion database: customer, item, quantity, unit price, and a status you set when a row is ready to bill.
- In Make or n8n, read the rows of that database that are ready (their Notion module or node reads database items).
- Map each row to the JSON body above, and send it with an HTTP request and the Idempotency-Key of the row.
- Store the returned editor_url where your workflow keeps its results.
The same steps work from any tool that can read your data and make an HTTP request.
Limits
| Limit | Value |
|---|---|
Requests for one key | 60 a minute and 5,000 a day (clock minutes and UTC days) |
Documents made through the API for one account | 500 a UTC day |
Your cloud documents | The same limit as in the editor, counting every document, whoever made it. A full cloud answers 403 document_limit_reached; delete documents or invite friends for room. |
Active keys | 5 for one account; at most 20 new keys a day |
Body of a request | 256 KB, 200 items |
Idempotency-Key | remembered for 24 hours |
The limits are counted in fixed windows, so a burst across a minute's end can briefly reach twice the figure. A request over a limit gets 429 with a Retry-After header: the seconds to wait.
Errors
An error is {"error": {"code": "...", "message": "..."}}. A request refused for its content (422 invalid_request) also lists the fields, as fields: [{"path", "message"}], up to 20 at a time. Nothing else is in an error: no trace, no internal id.
| Status | Code | Meaning |
|---|---|---|
400 | invalid_json | The body is not valid JSON or not valid UTF-8 text. |
400 | invalid_query | A parameter of the list is not one the endpoint has, or has a wrong value. |
400 | invalid_idempotency_key | The Idempotency-Key header is not 1 to 255 letters, digits, dots, dashes, underscores or colons. |
401 | invalid_api_key | The key is missing, wrong, revoked or expired. |
403 | insufficient_scope | The key does not have the scope the endpoint needs. |
403 | document_limit_reached | Your QuoteBill cloud is full. |
404 | not_found | No such endpoint, or no such document of yours. |
405 | method_not_allowed | The endpoint does not have that method (the Allow header lists them). |
409 | document_number_taken | Another of your documents has this document_number. |
409 | idempotency_key_reused | The Idempotency-Key was used for a different request in the last 24 hours. |
413 | payload_too_large | The body is over 256 KB. |
415 | unsupported_media_type | The body is not sent as application/json. |
422 | invalid_request | A field is missing, unknown or out of range, or a text holds characters that cannot be stored; the fields list says which. |
429 | rate_limited | A key made too many requests; wait Retry-After seconds. |
429 | daily_document_limit | The account has made 500 documents through the API today. |
500 | internal_error | Something went wrong on QuoteBill's side. Retry; if it persists, contact support. |
503 | service_unavailable | QuoteBill could not answer just now. Retry after a few seconds. |
Keeping a key safe
A key is a password for creating documents in your account. QuoteBill keeps only a fingerprint of it (a SHA-256 hash), so it cannot show you the key again.
- Store the key in the credential store of the tool that uses it, never in a sheet, a Notion page, a document, a chat message, a screenshot or a web address.
- Make one key for each workflow, named for it, so that you can revoke one without stopping the others.
- To rotate a key, make the new one, put it in the tool, check it, then revoke the old one.
- If a key may have been seen, revoke it at once in Company settings; it stops working on the next request.
- Look at "last used" in the list: a key you do not recognise, or one used when nothing should have run, is a reason to revoke it.
- Use the shortest validity that suits the job.
- Revoke keys you no longer use. Keys are not revoked automatically when you change your password.
What the API cannot do
- Send a document or an e-mail, or e-sign or send a contract; create or change a contract.
- Edit, replace or delete a document, or change its status. Open editor_url to do that.
- Read another member's documents, or anything of your account but your own documents.
- Change your account or company settings, or make or revoke keys. Keys are managed in Company settings only.
- Call webhooks or any other address: there are no webhooks and no calls to addresses you supply (the service reads only QuoteBill's own published template and tax data), so poll GET /v1/documents if you need to know about a change.
- Make a statement of account, or lines with an HS code, a country of origin, a discount or different tax rates; make those in the editor.
- Be called from a browser page: it sends no CORS headers.
A QuoteBill document is a printable document, not a registered e-invoice, whatever way it was made. A tax rate the API fills in is the published rate for the country, a starting point you should confirm.
Make, Zapier, n8n, Notion and Google Sheets are products and trademarks of their owners. QuoteBill is not affiliated with, endorsed by or listed in any of them, and has no partner status with them. The steps above use only their generic HTTP request features, and steps in those products may change.