Sumqo API
Read and add Sumqo records from your own servers and integrations with an API key, and get events as they happen with webhooks. Keys are created by the workspace in Settings → External access and are limited to the permissions, addresses and lifetime chosen when they were created.
Authentication
Send the key in the Authorization header of every request. Never in a URL.
curl https://connect.sumqo.com/v1/workspace \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Keys look like sumqo_live_ followed by 32 letters and digits, an underscore and a 6-character checksum. Treat a key like a password: keep it in a secret store, never in a browser, an app sent to customers, a repository or a chat. Calls from a web browser are refused.
GET /workspace needs no permission: it returns the workspace name, currency, time zone and the key's own permissions and expiry — a good first call.
Base URL
https://connect.sumqo.com/v1
The older address https://api.sumqo.com/connect/v1 still works but is deprecated — move your integration to the address above.
Every address below is relative to it. GET reads; POST and PATCH create and change records when the key has a permission that allows it. Write bodies are JSON (Content-Type: application/json).
Responses and pages
Every answer is JSON with the data under data. Lists add meta:
{
"data": [
{
"id": "5b9e…",
"invoiceNumber": "INV-1042",
"totalCents": 125000
}
],
"meta": {
"total": 1,
"count": 1,
"nextCursor": null
}
} Lists that page accept limit; when meta.nextCursor is not null, send it back as cursor to get the next page. Money is in cents (totalCents: 125000 = $1,250.00); dates are YYYY-MM-DD. Full tax IDs and bank account numbers are never returned.
Limits
- 60 requests per minute per key, counted in fixed one-minute windows (up to 60 may arrive at once).
- 300 requests per minute for all keys of one workspace together.
- Lists return 50 records per page unless you ask for a different
limit(at most 100). - Over the limit you get
429with aRetry-Afterheader in seconds. - Keys expire: 90 days by default, at most 365 days (180 for keys that can change records, 90 for high-risk permissions and recording payments).
IP address rules
A key can be limited to up to 20 IPv4 or IPv6 addresses or ranges (for example 198.51.100.24 or 198.51.100.0/24). Calls from any other address get 403 ip_not_allowed. Keys with high-risk permissions or that record payments must have rules, with ranges no wider than /24 (IPv4) or /48 (IPv6). Rules are recommended for every key that can change records: without them the workspace must confirm that anyone holding the key can change its books from anywhere. Rules protect you best when the key runs on your own servers; shared platforms call from addresses all their customers share.
Errors
Errors are JSON: {"error": {"code": "insufficient_scope", "message": "…"}}
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_query | A query parameter is not part of this address, or its value is not valid. |
| 400 | invalid_body | The body is not a JSON object, or a field has the wrong shape. |
| 400 | field_not_allowed | The body sets a field an API key may not set (for example a tax ID). |
| 400 | idempotency_key_missing | Writes need an Idempotency-Key header of 16 to 128 characters. |
| 400 | request_failed | Sumqo refused the change for a business reason; the message says why. |
| 401 | missing_key | No API key was sent. |
| 401 | invalid_key | The key is not a Sumqo key, or it does not exist. |
| 401 | key_revoked | The key was revoked. |
| 401 | key_expired | The key expired. |
| 403 | browser_not_allowed | Keys only work from servers, never from a web browser. |
| 403 | access_disabled | The workspace turned off all external access. |
| 403 | workspace_inactive | The workspace is not active. |
| 403 | ip_not_allowed | The call came from an address that is not on the key's list. |
| 403 | insufficient_scope | The key doesn't have the permission this address needs. |
| 403 | approval_unsupported | The workspace requires approval for records created by keys, and this kind of record cannot wait for approval. |
| 404 | not_found | There is no such address in the Sumqo API (or the record does not exist). |
| 405 | method_not_allowed | The address exists but not with this method; see the Allow header. |
| 409 | idempotency_key_reuse | This Idempotency-Key was already used with a different body or address. |
| 409 | request_in_progress | A request with this Idempotency-Key is still running; retry in a few seconds. |
| 409 | idempotency_outcome_unknown | The first request with this Idempotency-Key did not finish. Check whether the record was created before retrying with a new key. |
| 413 | body_too_large | The body is larger than this address accepts. |
| 415 | unsupported_media_type | Writes must send Content-Type: application/json. |
| 429 | rate_limited | Too many requests; wait the number of seconds in Retry-After. |
| 500 | internal_error | Something went wrong on our side. The same Idempotency-Key replays this answer; check whether the record was created before retrying with a new key. |
What you can read
Each permission unlocks only the addresses listed under it.
Products and services
GET /itemsList products and servicesParameters: status, type, searchGET /items/{itemId}Read a product or service
curl https://connect.sumqo.com/v1/items \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
{
"data": [
{
"id": "a1f0…",
"name": "Website maintenance",
"type": "service",
"rateCents": 12000,
"status": "active"
}
],
"meta": {
"total": 1,
"count": 1,
"nextCursor": null
}
}Categories
GET /accountsList categoriesGET /accounts/{accountId}Read a category
curl https://connect.sumqo.com/v1/accounts \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
{
"data": [
{
"id": "c7d2…",
"name": "Office supplies",
"type": "expense",
"accountNumber": "6100"
}
],
"meta": {
"total": 1,
"count": 1,
"nextCursor": null
}
}Invoices
Anyone holding this key can read every invoice, amount and customer you billed.
GET /invoicesList invoicesParameters: limit, cursor, status, customerId, from, to, searchGET /invoices/{invoiceId}Read an invoiceGET /estimatesList estimatesParameters: limit, cursor, status, customerId, from, to, searchGET /estimates/{estimateId}Read an estimateGET /credit-notesList credit notesParameters: limit, cursor, status, customerId, from, toGET /credit-notes/{creditNoteId}Read a credit noteGET /recurring-invoicesList recurring invoicesParameters: status
curl https://connect.sumqo.com/v1/invoices \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
{
"data": [
{
"id": "5b9e…",
"invoiceNumber": "INV-1042",
"customerName": "Northwind Bakery",
"txnDate": "2026-09-02",
"dueDate": "2026-10-02",
"totalCents": 125000,
"balanceCents": 125000,
"status": "sent"
}
],
"meta": {
"total": 1,
"count": 1,
"nextCursor": null
}
}Payments received
Anyone holding this key can see who paid you and how much.
GET /paymentsList payments receivedParameters: customerId, from, toGET /payments/{paymentId}Read a payment
curl https://connect.sumqo.com/v1/payments \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
{
"data": [
{
"id": "9a41…",
"customerName": "Northwind Bakery",
"amountCents": 50000,
"date": "2026-09-15",
"method": "ach"
}
],
"meta": {
"total": 1,
"count": 1,
"nextCursor": null
}
}Income
Anyone holding this key can see your income line by line.
GET /incomeList incomeParameters: limit, cursor, customerId, accountId, from, to, searchGET /income/{incomeId}Read an income record
curl https://connect.sumqo.com/v1/income \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Expenses
Anyone holding this key can see what you spend and whom you pay.
GET /expensesList expensesParameters: limit, cursor, vendorId, accountId, from, to, searchGET /expenses/{expenseId}Read an expense
curl https://connect.sumqo.com/v1/expenses \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
{
"data": [
{
"id": "3e77…",
"vendorName": "Office Depot",
"amountCents": 8999,
"date": "2026-09-20",
"accountName": "Office supplies"
}
],
"meta": {
"total": 1,
"count": 1,
"nextCursor": null
}
}Bills
Anyone holding this key can see the bills you owe and to whom.
GET /billsList billsParameters: limit, cursor, status, vendorId, from, toGET /bills/{billId}Read a bill
curl https://connect.sumqo.com/v1/bills \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Bill payments
Anyone holding this key can see when and how much you paid your vendors.
GET /bill-paymentsList bill paymentsParameters: vendorId, from, toGET /bill-payments/{billPaymentId}Read a bill payment
curl https://connect.sumqo.com/v1/bill-payments \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Reports
Anyone holding this key can see your full financial picture (profit, cash, debts).
GET /reports/profit-lossRead the profit and loss reportParameters: from, to, basisGET /reports/ar-agingRead who owes you (receivables aging)Parameters: asOfGET /reports/ap-agingRead what you owe (payables aging)Parameters: asOfGET /reports/summaryRead the yearly summaryParameters: year
curl https://connect.sumqo.com/v1/reports/profit-loss \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Customers
Anyone holding this key can download your customer list with names, emails, phones and addresses.
GET /customersList customersParameters: limit, cursor, status, searchGET /customers/{customerId}Read a customer
curl https://connect.sumqo.com/v1/customers \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
{
"data": [
{
"id": "d3c1…",
"displayName": "Northwind Bakery",
"email": "billing@northwind.example",
"phone": "+1 555 0100"
}
],
"meta": {
"total": 1,
"count": 1,
"nextCursor": null
}
}Vendors
Anyone holding this key can download your vendor and contractor list with contact details.
GET /vendorsList vendorsParameters: limit, cursor, status, type, searchGET /vendors/{vendorId}Read a vendor
curl https://connect.sumqo.com/v1/vendors \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Banking
Anyone holding this key can read your bank transactions line by line.
GET /bank-accountsList bank and card accountsParameters: statusGET /bank-accounts/{accountId}Read a bank or card accountGET /bank-transactionsList bank transactionsParameters: accountId, direction, matchStatus, from, to, search
curl https://connect.sumqo.com/v1/bank-accounts \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Downloads
Lets the key download your whole customer list in one file.
GET /exports/customersDownload the customer listParameters: format, status, from, to, search
curl https://connect.sumqo.com/v1/exports/customers \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Lets the key download your whole vendor list in one file.
GET /exports/vendorsDownload the vendor listParameters: format, status, from, to, search
curl https://connect.sumqo.com/v1/exports/vendors \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Lets the key download every invoice in one file.
GET /exports/invoicesDownload invoicesParameters: format, status, from, to, search
curl https://connect.sumqo.com/v1/exports/invoices \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Lets the key download every expense in one file.
GET /exports/expensesDownload expensesParameters: format, status, from, to, search
curl https://connect.sumqo.com/v1/exports/expenses \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Lets the key download every bill in one file.
GET /exports/billsDownload billsParameters: format, status, from, to, search
curl https://connect.sumqo.com/v1/exports/bills \ -H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3"
Writes
These permissions let a key add records and change some of them. Every write needs an Idempotency-Key header (see Idempotency), and only the fields listed are accepted — anything else is refused with 400. Every change is recorded in the workspace's history under the key's name.
customers:writeIP rules recommended · max 180 daysA leaked key could add customers or change a customer's email address so invoices go to someone else. Tax IDs can't be set with a key.
POST /customersCreate a customerRequiresIdempotency-Key.Field Required Notes displayName Yes companyName No firstName No lastName No email No Main email address phone No mobilePhone No fax No altPhone No website No category No notes No paymentTerms No paymentTerms.termsType Yes One of: due_on_receipt, net, custom. paymentTerms.netDays No paymentTerms.label No billingAddress No billingAddress.line1 No billingAddress.line2 No billingAddress.city No billingAddress.state No billingAddress.postalCode No billingAddress.country No shippingAddress No shippingAddress.line1 No shippingAddress.line2 No shippingAddress.city No shippingAddress.state No shippingAddress.postalCode No shippingAddress.country No shippingSameAsBilling No creditLimitCents No Credit limit in cents contacts[] No A list. contacts[].name Yes contacts[].role No contacts[].email No contacts[].phone No contacts[].notes No PATCH /customers/{customerId}Change a customerRequiresIdempotency-Key.Field Required Notes displayName No companyName No firstName No lastName No email No Main email address phone No mobilePhone No fax No altPhone No website No category No notes No paymentTerms No paymentTerms.termsType Yes One of: due_on_receipt, net, custom. paymentTerms.netDays No paymentTerms.label No billingAddress No billingAddress.line1 No billingAddress.line2 No billingAddress.city No billingAddress.state No billingAddress.postalCode No billingAddress.country No shippingAddress No shippingAddress.line1 No shippingAddress.line2 No shippingAddress.city No shippingAddress.state No shippingAddress.postalCode No shippingAddress.country No shippingSameAsBilling No creditLimitCents No Credit limit in cents
curl -X POST https://connect.sumqo.com/v1/customers \
-H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f0c9a52-6e1b-4d7a-b8c2-9e4f1a7d2b60" \
-d '{"displayName":"<displayName>"}'vendors:writeIP rules recommended · max 180 daysA leaked key could add vendors or change a vendor's contact details. Tax IDs, 1099 settings and account numbers can't be set with a key.
POST /vendorsCreate a vendorRequiresIdempotency-Key.Field Required Notes displayName Yes companyName No firstName No lastName No email No Main email address phone No mobilePhone No fax No altPhone No website No category No notes No paymentTerms No paymentTerms.termsType Yes One of: due_on_receipt, net, custom. paymentTerms.netDays No paymentTerms.label No address No address.line1 No address.line2 No address.city No address.state No address.postalCode No address.country No defaultAccountId No Default expense category PATCH /vendors/{vendorId}Change a vendorRequiresIdempotency-Key.Field Required Notes displayName No companyName No firstName No lastName No email No Main email address phone No mobilePhone No fax No altPhone No website No category No notes No paymentTerms No paymentTerms.termsType Yes One of: due_on_receipt, net, custom. paymentTerms.netDays No paymentTerms.label No address No address.line1 No address.line2 No address.city No address.state No address.postalCode No address.country No defaultAccountId No Default expense category
curl -X POST https://connect.sumqo.com/v1/vendors \
-H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f0c9a52-6e1b-4d7a-b8c2-9e4f1a7d2b60" \
-d '{"displayName":"<displayName>"}'items:writeIP rules recommended · max 180 daysA leaked key could change the names and prices of what you sell.
POST /itemsCreate a product or serviceRequiresIdempotency-Key.Field Required Notes name Yes unitPriceCents Yes description No type No One of: service, product. incomeAccountId No Income category taxable No PATCH /items/{itemId}Change a product or serviceRequiresIdempotency-Key.Field Required Notes name No unitPriceCents No description No type No One of: service, product. incomeAccountId No Income category taxable No
curl -X POST https://connect.sumqo.com/v1/items \
-H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f0c9a52-6e1b-4d7a-b8c2-9e4f1a7d2b60" \
-d '{"name":"<name>","unitPriceCents":"<unitPriceCents>"}'invoices:writeIP rules recommended · max 180 daysA leaked key could create invoices (as drafts or issued) that change what your customers owe you. It can never send, void or delete them.
POST /invoicesCreate an invoiceRequiresIdempotency-Key.Field Required Notes customerId Yes txnDate Yes Invoice date (YYYY-MM-DD) dueDate No YYYY-MM-DD; default from the customer’s payment terms status No draft (default) or sent (issued, not emailed) One of: draft, sent. lines[] Yes A list. lines[].itemId No lines[].description Yes lines[].quantity Yes lines[].rateCents Yes Price per unit in cents lines[].kind No One of: item, discount. lines[].taxable No lines[].taxRateBp No Tax rate in basis points (825 = 8.25 %) lines[].taxFixedCents No taxCents No memo No Message shown to the customer internalNotes No
curl -X POST https://connect.sumqo.com/v1/invoices \
-H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f0c9a52-6e1b-4d7a-b8c2-9e4f1a7d2b60" \
-d '{"customerId":"<customerId>","txnDate":"<txnDate>","lines":[{"description":"<description>","quantity":"<quantity>","rateCents":"<rateCents>"}]}'payments:writeNeeds IP rules · max 180 daysA leaked key could mark invoices as paid. Keys with this permission only work from the addresses you list.
POST /paymentsRecord a paymentRequiresIdempotency-Key.Field Required Notes customerId Yes txnDate Yes Date received (YYYY-MM-DD) amountCents Yes method No One of: cash, check, card, bank_transfer, other. referenceNumber No instrumentDate No Date written on the check or transfer order (YYYY-MM-DD), reference only memo No depositTo No One of: account, undeposited. appliedTo[] No Invoices the payment pays A list. appliedTo[].invoiceId Yes appliedTo[].amountCents Yes POST /sales-receiptsCreate a sales receiptRequiresIdempotency-Key.Also needs: invoices:writeField Required Notes customerId No Default: the walk-in customer txnDate Yes lines[] Yes A list. lines[].itemId No lines[].description Yes lines[].quantity Yes lines[].rateCents Yes Price per unit in cents lines[].kind No One of: item, discount. lines[].taxable No lines[].taxRateBp No Tax rate in basis points (825 = 8.25 %) lines[].taxFixedCents No taxCents No memo No payment Yes payment.method Yes One of: cash, check, card, bank_transfer, other. payment.date No payment.depositTo No One of: account, undeposited. payment.reference No
curl -X POST https://connect.sumqo.com/v1/payments \
-H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f0c9a52-6e1b-4d7a-b8c2-9e4f1a7d2b60" \
-d '{"customerId":"<customerId>","txnDate":"<txnDate>","amountCents":"<amountCents>"}'expenses:writeIP rules recommended · max 180 daysA leaked key could add expenses that change your profit and your taxes.
POST /expensesCreate an expenseRequiresIdempotency-Key.Field Required Notes accountId Yes Expense category vendorId No txnDate Yes amountCents Yes paymentMethod No One of: cash, check, card, bank_transfer, other. paymentMethodLabel No referenceNumber No memo No classId No locationId No
curl -X POST https://connect.sumqo.com/v1/expenses \
-H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f0c9a52-6e1b-4d7a-b8c2-9e4f1a7d2b60" \
-d '{"accountId":"<accountId>","txnDate":"<txnDate>","amountCents":"<amountCents>"}'bills:writeIP rules recommended · max 180 daysA leaked key could add bills you owe. It can never pay or approve them.
POST /billsCreate a billRequiresIdempotency-Key.Field Required Notes vendorId Yes txnDate Yes dueDate Yes billNumber No memo No lines[] Yes A list. lines[].accountId Yes Expense category lines[].description No lines[].amountCents Yes lines[].itemId No lines[].quantity No lines[].classId No lines[].locationId No
curl -X POST https://connect.sumqo.com/v1/bills \
-H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f0c9a52-6e1b-4d7a-b8c2-9e4f1a7d2b60" \
-d '{"vendorId":"<vendorId>","txnDate":"<txnDate>","dueDate":"<dueDate>","lines":[{"accountId":"<accountId>","amountCents":"<amountCents>"}]}'documents:writeIP rules recommended · max 180 daysA leaked key could upload files into your workspace. They are kept without reading unless you turn on reading, and never posted.
POST /documentsUpload a documentRequiresIdempotency-Key.Field Required Notes fileName Yes mimeType Yes One of: application/pdf, image/jpeg, image/png, image/webp, image/heic, image/heif. contentBase64 Yes The file, base64-encoded (at most 4 MB) processing No store_only (default): keep the file without reading it; review: read it and put it in Documents to review — only when the workspace turned on reading of documents uploaded with keys (at most 30 a day per key) One of: review, store_only. note No
curl -X POST https://connect.sumqo.com/v1/documents \
-H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f0c9a52-6e1b-4d7a-b8c2-9e4f1a7d2b60" \
-d '{"fileName":"<fileName>","mimeType":"application/pdf","contentBase64":"<contentBase64>"}'webhooks:manageIP rules recommended · max 180 daysA leaked key could send notices about your new invoices, payments and other records to an address of its choice. Notices carry ids and amounts, never names or contact details.
POST /hooksSubscribe to eventsRequiresIdempotency-Key.Field Required Notes url Yes https address that receives the events events[] No Event types (see Webhooks) A list. event No One event type (Zapier style) DELETE /hooks/{hookId}Unsubscribe from events
curl -X POST https://connect.sumqo.com/v1/hooks \
-H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f0c9a52-6e1b-4d7a-b8c2-9e4f1a7d2b60" \
-d '{"url":"<url>"}'Idempotency
Networks fail: a write can succeed while its answer is lost. Send a unique Idempotency-Key header with every POST and PATCH — 16 to 128 characters, for example a UUID — and retry with the same value.
- For 24 hours, a retry with the same key and the same body gets the first answer again instead of creating a second record. That answer carries
Idempotent-Replayed: true. - The same key with a different body (or another address) is refused with
409 idempotency_key_reuse. - A retry that arrives while the first call is still running gets
409 request_in_progress— wait a moment and retry. - A write without the header is refused with
400.
Approvals
A workspace can ask for records created by API keys to be approved by a person (for every key, or only for some keys). When that applies to your key:
- Expenses and bills are created but wait for approval; they don't count in the books until approved. The answer includes
"pendingApproval": true. - Invoices are always created as drafts — nothing is sent to the customer.
- Payments are not recorded yet: the answer is
202 Acceptedwith"pendingApproval": true, and the payment is recorded only when someone approves it (you'll getpayment.receivedby webhook then). - Sales receipts are refused, because they record money received at once.
{
"data": {
"pendingApproval": true,
"message": "The payment waits for approval in the workspace."
}
}Webhooks
The workspace adds up to 5 endpoints in Settings → External access → Webhooks. Each event is sent as a POST with a JSON body:
{
"id": "evt_2Jd8x0aQ…",
"type": "invoice.created",
"createdAt": "2026-10-01T15:04:05.000Z",
"data": {
"object": "invoice",
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"invoiceNumber": "INV-1042",
"status": "draft",
"totalCents": 125000,
"paidCents": 0,
"balanceCents": 125000,
"currency": "USD",
"txnDate": "2026-10-01",
"dueDate": "2026-10-31",
"customerId": "a1b2c3d4-0000-4000-8000-000000000002"
},
"workspace": {
"id": "9c1f…",
"name": "Acme Plumbing"
}
}Delivery
- Headers:
Content-Type: application/json,User-Agent: Sumqo-Webhooks/1.0,Sumqo-Event-Id,Sumqo-Event-TypeandSumqo-Signature. - Answer with any
2xxwithin 5 seconds. Anything else, or no answer, is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. - An endpoint that keeps failing for 3 days is turned off, and the workspace is told.
- The same event can arrive more than once and events can arrive out of order: use
idto skip ones you already handled. - Bodies never carry tax IDs, bank account numbers or other secrets — fetch the record with your key when you need more.
Checking the signature
Sumqo-Signature: t=1759330800,v1=5257a869… — t is when it was sent (Unix seconds) and v1 is the hex HMAC-SHA256 of t + "." + body with your endpoint's signing secret.
- Hash the raw body bytes exactly as received — parsing and re-serializing the JSON changes them.
- Accept the delivery if any
v1matches: while a secret is being rotated (24 hours) each delivery carries one signature per secret. - Reject it if
tis more than 5 minutes (300 seconds) away from your clock — that stops replays. - Compare in constant time.
// Node.js 18+ — no dependencies. Run: SUMQO_WEBHOOK_SECRET=whsec_... node server.js
const crypto = require('node:crypto');
const http = require('node:http');
const TOLERANCE_SECONDS = 300;
/**
* True when rawBody (a Buffer with the exact bytes received) was signed by Sumqo.
* header = the Sumqo-Signature header, secret = your endpoint's signing secret (whsec_...).
*/
function verifySumqoSignature(rawBody, header, secret, nowSeconds = Math.floor(Date.now() / 1000)) {
if (typeof header !== 'string' || !secret) return false;
let timestamp = null;
const signatures = [];
for (const part of header.split(',')) {
const i = part.indexOf('=');
if (i === -1) continue;
const key = part.slice(0, i).trim();
const value = part.slice(i + 1).trim();
if (key === 't') timestamp = value;
else if (key === 'v1') signatures.push(value);
}
if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0) return false;
if (Math.abs(nowSeconds - Number(timestamp)) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(Buffer.concat([Buffer.from(timestamp + '.', 'utf8'), rawBody]))
.digest();
// Accept if ANY v1 matches: during a secret rotation each delivery carries two.
return signatures.some((sig) => {
if (!/^[0-9a-fA-F]{64}$/.test(sig)) return false;
const given = Buffer.from(sig, 'hex');
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
}
http
.createServer((req, res) => {
if (req.method !== 'POST') {
res.writeHead(405);
return res.end();
}
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const rawBody = Buffer.concat(chunks); // the exact bytes — never JSON.stringify(parsed)
const ok = verifySumqoSignature(rawBody, req.headers['sumqo-signature'], process.env.SUMQO_WEBHOOK_SECRET);
if (!ok) {
res.writeHead(400);
return res.end('Invalid signature');
}
const event = JSON.parse(rawBody.toString('utf8'));
// Deliveries can repeat: skip an event.id you have already handled.
console.log('Received', event.type, event.id);
res.writeHead(200);
res.end('ok');
});
})
.listen(3000, () => console.log('Listening on http://localhost:3000'));
Events
invoice.createdInvoice createdA new invoice was saved (draft or sent).
{
"object": "invoice",
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"invoiceNumber": "INV-1042",
"status": "draft",
"totalCents": 125000,
"paidCents": 0,
"balanceCents": 125000,
"currency": "USD",
"txnDate": "2026-10-01",
"dueDate": "2026-10-31",
"customerId": "a1b2c3d4-0000-4000-8000-000000000002"
}invoice.sentInvoice sentAn invoice was emailed to the customer or marked as sent.
{
"object": "invoice",
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"invoiceNumber": "INV-1042",
"status": "sent",
"totalCents": 125000,
"paidCents": 0,
"balanceCents": 125000,
"currency": "USD",
"txnDate": "2026-10-01",
"dueDate": "2026-10-31",
"customerId": "a1b2c3d4-0000-4000-8000-000000000002"
}invoice.paidInvoice paidA payment (or an online payment, or an applied credit note) brought an invoice to a zero balance.
{
"object": "invoice",
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"invoiceNumber": "INV-1042",
"status": "paid",
"totalCents": 125000,
"paidCents": 125000,
"balanceCents": 0,
"currency": "USD",
"txnDate": "2026-10-01",
"dueDate": "2026-10-31",
"customerId": "a1b2c3d4-0000-4000-8000-000000000002",
"paidAt": "2026-10-20"
}invoice.voidedInvoice voidedAn invoice was voided.
{
"object": "invoice",
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"invoiceNumber": "INV-1042",
"status": "void",
"totalCents": 125000,
"paidCents": 0,
"balanceCents": 125000,
"currency": "USD",
"txnDate": "2026-10-01",
"dueDate": "2026-10-31",
"customerId": "a1b2c3d4-0000-4000-8000-000000000002"
}payment.receivedPayment receivedA customer payment was recorded (by hand, online or from a bank deposit).
{
"object": "payment",
"id": "a1b2c3d4-0000-4000-8000-000000000003",
"status": "active",
"amountCents": 125000,
"currency": "USD",
"txnDate": "2026-10-20",
"method": "card",
"customerId": "a1b2c3d4-0000-4000-8000-000000000002",
"invoiceIds": [
"a1b2c3d4-0000-4000-8000-000000000001"
]
}payment.voidedPayment voidedA customer payment was voided: the invoices it paid are open again for the amount it covered. `invoiceIds` names them.
{
"object": "payment",
"id": "a1b2c3d4-0000-4000-8000-000000000003",
"status": "voided",
"amountCents": 125000,
"currency": "USD",
"txnDate": "2026-10-20",
"method": "card",
"customerId": "a1b2c3d4-0000-4000-8000-000000000002",
"invoiceIds": [
"a1b2c3d4-0000-4000-8000-000000000001"
]
}expense.createdExpense createdA new expense was recorded.
{
"object": "expense",
"id": "a1b2c3d4-0000-4000-8000-000000000004",
"amountCents": 4599,
"currency": "USD",
"txnDate": "2026-10-02",
"paymentMethod": "card",
"accountId": "a1b2c3d4-0000-4000-8000-000000000005",
"vendorId": "a1b2c3d4-0000-4000-8000-000000000011"
}bill.createdBill createdA new bill from a vendor was recorded.
{
"object": "bill",
"id": "a1b2c3d4-0000-4000-8000-000000000010",
"billNumber": "B-77",
"status": "open",
"totalCents": 48000,
"paidCents": 0,
"balanceCents": 48000,
"currency": "USD",
"txnDate": "2026-10-01",
"dueDate": "2026-10-15",
"vendorId": "a1b2c3d4-0000-4000-8000-000000000011"
}bill.paidBill paidA bill payment (or an applied vendor credit) brought a bill to a zero balance.
{
"object": "bill",
"id": "a1b2c3d4-0000-4000-8000-000000000010",
"billNumber": "B-77",
"status": "paid",
"totalCents": 48000,
"paidCents": 48000,
"balanceCents": 0,
"currency": "USD",
"txnDate": "2026-10-01",
"dueDate": "2026-10-15",
"vendorId": "a1b2c3d4-0000-4000-8000-000000000011"
}bill_payment.voidedBill payment voidedA payment to a vendor was voided: the bills it paid are open again for the amount it covered. `billIds` names them.
{
"object": "bill_payment",
"id": "a1b2c3d4-0000-4000-8000-000000000012",
"status": "voided",
"amountCents": 48000,
"currency": "USD",
"txnDate": "2026-10-16",
"method": "check",
"vendorId": "a1b2c3d4-0000-4000-8000-000000000011",
"billIds": [
"a1b2c3d4-0000-4000-8000-000000000010"
]
}customer.createdCustomer createdA new customer was added.
{
"object": "customer",
"id": "a1b2c3d4-0000-4000-8000-000000000002",
"active": true
}customer.updatedCustomer updatedA customer was changed. `changedFields` names what changed (never the values).
{
"object": "customer",
"id": "a1b2c3d4-0000-4000-8000-000000000002",
"active": true,
"changedFields": [
"email",
"phone"
]
}vendor.createdVendor createdA new vendor was added.
{
"object": "vendor",
"id": "a1b2c3d4-0000-4000-8000-000000000011",
"active": true
}vendor.updatedVendor updatedA vendor was changed. `changedFields` names what changed (never the values).
{
"object": "vendor",
"id": "a1b2c3d4-0000-4000-8000-000000000011",
"active": true,
"changedFields": [
"paymentTerms"
]
}sales_receipt.createdSales receipt createdA sale was recorded and paid in one step.
{
"object": "sales_receipt",
"id": "a1b2c3d4-0000-4000-8000-000000000006",
"number": "SR-1007",
"status": "paid",
"totalCents": 8900,
"currency": "USD",
"txnDate": "2026-10-03",
"customerId": "a1b2c3d4-0000-4000-8000-000000000002",
"paymentId": "a1b2c3d4-0000-4000-8000-000000000007"
}statement.importedBank statement importedA bank or card statement file was imported (or imported again with new transactions).
{
"object": "statement",
"id": "a1b2c3d4-0000-4000-8000-000000000008",
"accountId": "a1b2c3d4-0000-4000-8000-000000000009",
"periodStart": "2026-09-01",
"periodEnd": "2026-09-30",
"transactionsAdded": 42,
"alreadyImported": 0
}A ping event is sent when someone clicks "Send test event"; it can't be subscribed to.
REST hooks (Zapier and similar)
A key with the webhooks:manage permission can add and remove its own endpoints, so an integration can subscribe when it is turned on:
POST /hookswith{"url": "https://hooks.example.com/…", "events": ["invoice.paid"]}→201with the hook'sid, its events and its signingsecret(shown once).DELETE /hooks/{hookId}removes it. A key can only remove hooks it added itself.- Hooks count towards the 5-endpoint limit, show in the workspace's settings as "Added by API key · name", and stop when the key is revoked.
curl -X POST https://connect.sumqo.com/v1/hooks \
-H "Authorization: Bearer sumqo_live_EXAMPLEkeyEXAMPLEkeyEXAMPLEke_a1B2c3" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: b71e4d09-2c5a-4f83-9d16-7a0e3c8b5f24" \
-d '{"url":"https://hooks.example.com/catch/123/abc","events":["invoice.paid"]}'AI assistants (MCP)
Sumqo runs a remote Model Context Protocol server (Streamable HTTP, stateless) so Claude, ChatGPT, Cursor and other AI assistants can read a workspace's books and propose changes. Server address: https://connect.sumqo.com/mcp.
- Sign in with Sumqo (OAuth 2.1). The assistant discovers Sumqo's authorization server from the server's protected-resource metadata; the person chooses the workspace and what the assistant may do. PKCE (S256) is required, tokens are bound to this server, access tokens last one hour and refresh tokens rotate on every use. Apps can identify themselves with a Client ID Metadata Document or register dynamically (shown as unverified).
- Or an API key as
Authorization: Bearer sumqo_live_…for tools that take a header — the key's own permissions, address rules and limits apply. - Changes are proposals. Every
propose_*tool files a request in the workspace's Needs review queue and answers "Waiting for approval"; nothing changes until a person approves it. - Results are data. Free text written by other people (memos, notes, descriptions, document text) comes back as
{"untrusted_text": …}and must never be treated as instructions. - No tool sends email, pays, voids, deletes, downloads files or manages webhooks.
| Tool | What it does | Permission |
|---|---|---|
| list_products_and_services | List products and services | items:readView |
| get_product_or_service | Get product or service | items:readView |
| list_categories | List categories | accounts:readView |
| get_category | Get category | accounts:readView |
| list_invoices | List invoices | invoices:readConfidential |
| get_invoice | Get invoice | invoices:readConfidential |
| list_estimates | List estimates | invoices:readConfidential |
| get_estimate | Get estimate | invoices:readConfidential |
| list_credit_notes | List credit notes | invoices:readConfidential |
| get_credit_note | Get credit note | invoices:readConfidential |
| list_recurring_invoices | List recurring invoices | invoices:readConfidential |
| list_payments_received | List payments received | payments:readConfidential |
| get_payment_received | Get payment received | payments:readConfidential |
| list_income | List income | income:readConfidential |
| get_income | Get income | income:readConfidential |
| list_expenses | List expenses | expenses:readConfidential |
| get_expense | Get expense | expenses:readConfidential |
| list_bills | List bills | bills:readConfidential |
| get_bill | Get bill | bills:readConfidential |
| list_bill_payments | List bill payments | bill_payments:readConfidential |
| get_bill_payment | Get bill payment | bill_payments:readConfidential |
| report_profit_and_loss | Report profit and loss | reports:readConfidential |
| report_receivables_aging | Report receivables aging | reports:readConfidential |
| report_payables_aging | Report payables aging | reports:readConfidential |
| report_yearly_summary | Report yearly summary | reports:readConfidential |
| list_customers | List customers | customers:readHigh risk |
| get_customer | Get customer | customers:readHigh risk |
| list_vendors | List vendors | vendors:readHigh risk |
| get_vendor | Get vendor | vendors:readHigh risk |
| list_bank_accounts | List bank accounts | banking:readHigh risk |
| get_bank_account | Get bank account | banking:readHigh risk |
| list_bank_transactions | List bank transactions | banking:readHigh risk |
| propose_new_customer | Propose new customer Waits for approval | customers:writeChanges your books |
| propose_customer_change | Propose customer change Waits for approval | customers:writeChanges your books |
| propose_new_vendor | Propose new vendor Waits for approval | vendors:writeChanges your books |
| propose_vendor_change | Propose vendor change Waits for approval | vendors:writeChanges your books |
| propose_new_product_or_service | Propose new product or service Waits for approval | items:writeChanges your books |
| propose_product_or_service_change | Propose product or service change Waits for approval | items:writeChanges your books |
| propose_invoice | Propose invoice Waits for approval | invoices:writeChanges your books |
| propose_payment_received | Propose payment received Waits for approval | payments:writeChanges your books |
| propose_expense | Propose expense Waits for approval | expenses:writeChanges your books |
| propose_bill | Propose bill Waits for approval | bills:writeChanges your books |
| profit_and_loss_by_category | Profit and loss by category | reports:readConfidential |
| aging_summary | Who owes what (aging) | reports:readConfidential |
| business_overview | Business overview | reports:readConfidential |
| cash_flow_forecast | Cash-flow forecast | reports:readConfidential |
Not available to API keys
Whatever their permissions, keys can never:
- Void, delete or restore anything
- Pay bills or record any other money going out
- Change bank accounts, bank transactions or reconciliations
- Approve bills or expenses
- Send emails to customers or vendors
- Post journal entries or change the chart of accounts
- Prepare or change tax filings, or see full tax IDs
- Change workspace settings, payment or payout details, people or permissions
- Download a full backup of the workspace
A person signed in to Sumqo does these.