Developers
Evenbooks API
Your books, from your own code. Nearly everything the app does is available over a JSON API, on every plan, with webhooks for the changes you care about.
- Base URL
https://evenbooks.org/api/v1- Authentication
Authorization: Bearer evb_…- Version
2026-10-19, sent as theapi-versionheader- OpenAPI 3.1
- openapi.json · 167 endpoints
What you can do
The Evenbooks app runs on this API, so a script can do nearly everything a person can: create and send invoices, record payments and expenses, review bank transactions, keep the chart of accounts, lock periods and pull reports (the few exceptions are under What API keys cannot do). Each area below opens its endpoints.
- Account1 endpointWho is calling and with what access.
- Company12 endpointsBusiness details and settings.
- Customers & vendors6 endpointsPeople and businesses you sell to and buy from.
- Products & services3 endpointsReusable invoice lines.
- Invoices14 endpointsDrafts, issuing, sending, PDFs and public links.
- Credit notes8 endpointsCredits against invoices.
- Payments received6 endpointsMoney received from customers and how it is applied to invoices.
- Refunds3 endpointsMoney returned to customers.
- Recurring invoices8 endpointsInvoices issued on a schedule.
- Expenses4 endpointsMoney spent (purchases, bills paid, vendor refunds).
- Banking41 endpointsBank accounts, connections, statement imports, bank transactions, rules and reconciliation.
- Journal entries9 endpointsThe ledger itself, including manual entries and starting balances.
- Chart of accounts3 endpointsCategories and balance sheet accounts.
- Sales tax3 endpointsSales tax rates.
- Tax form lines3 endpointsWhich Schedule C line each category reports on.
- Accounting periods4 endpointsClosing and reopening periods.
- Reports9 endpointsFinancial reports as JSON, CSV or PDF.
- Imports6 endpointsImporting history from another app.
- Attachments5 endpointsReceipts and other files attached to records.
- Online payments8 endpointsCard and bank payments through Stripe.
- History1 endpointEvery change, who made it and when.
- Background jobs1 endpointEmail, PDF, sync and recurring work the server does later.
- Webhooks9 endpointsGet notified when something changes.
Quick start
From a new key to an issued invoice in five requests. The examples use curl and, in one place, jq.
Step 1: Create a key
An administrator opens Settings → Developers, chooses Create API key, names it after what will use it (“Invoicing script”) and picks a role and access. Copy the key when it is shown. We show it once and keep only a hash, so we can’t show it again.
Step 2: Check that it works
GET /metells you which company the key belongs to, what it may do and its request budget.Request export EVENBOOKS_API_KEY="evb_…" # from Settings → Developers curl https://evenbooks.org/api/v1/me \ -H "Authorization: Bearer $EVENBOOKS_API_KEY"Response { "apiVersion": "2026-10-19", "company": { "id": "5f8077c8-1bae-4bc0-9183-2b10396edfb3", "name": "Harbor Light Studio", "baseCurrency": "USD", "timeZone": "America/New_York", … }, "role": "member", "auth": { "type": "api_key", "keyId": "bb02cf19-f42a-49a9-a9b8-a2783b29fe40", "name": "Invoicing script", "access": "write", "expiresAt": null }, "rateLimit": { "perMinute": 120, "perDay": 10000 } }Step 3: Find the income category
Every invoice line says which income category it counts toward.
GET /accountsreturns the chart of accounts as a list; keep theidof anincomeaccount such as 4000 Sales.Request curl https://evenbooks.org/api/v1/accounts \ -H "Authorization: Bearer $EVENBOOKS_API_KEY" \ | jq '.[] | select(.type == "income") | {id, code, name}'Output { "id": "afb21c42-f1d2-4cca-a1d6-5686f474c28c", "code": "4000", "name": "Sales" } { "id": "df5c4393-06cf-4f91-818b-c281c06abe0c", "code": "4100", "name": "Service Revenue" } …Step 4: Add a customer
POST /contactswith aname.isCustomeris true unless you say otherwise; setisVendorfor someone you also buy from.Request curl -X POST https://evenbooks.org/api/v1/contacts \ -H "Authorization: Bearer $EVENBOOKS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Supply Co.", "email": "billing@acme.example", "isCustomer": true }'Response { "id": "51041bb7-1b7b-4c1d-809e-560e6d215f70", "name": "Acme Supply Co.", "isCustomer": true, "isVendor": false, "email": "billing@acme.example", "version": 1, … }Step 5: Create a draft invoice
POST /invoicesneeds the customer, an issue date and at least one line. GivetermsDaysor adueDate; without either, the company’s default terms apply. Amounts are strings.Request curl -X POST https://evenbooks.org/api/v1/invoices \ -H "Authorization: Bearer $EVENBOOKS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contactId": "51041bb7-1b7b-4c1d-809e-560e6d215f70", "issueDate": "2026-10-19", "termsDays": 30, "lines": [ { "description": "Website redesign, October", "quantity": "1", "unitPrice": "1250.00", "incomeAccountId": "afb21c42-f1d2-4cca-a1d6-5686f474c28c" } ] }'Response { "id": "4436acc5-82a4-4139-9f55-4988b6b2fc61", "number": null, "status": "draft", "issueDate": "2026-10-19", "dueDate": "2026-11-18", "total": "1250.00", "balanceDue": "1250.00", "version": 1, … }A draft is not in your books yet. You can change it freely with
PATCH /invoices/{id}.Step 6: Issue it
POST /invoices/{id}/issuewith the draft’sversion. Issuing gives the invoice its number and records it in your books. Send anIdempotency-Keyso a retry after a timeout can’t issue it twice.Request curl -X POST https://evenbooks.org/api/v1/invoices/4436acc5-82a4-4139-9f55-4988b6b2fc61/issue \ -H "Authorization: Bearer $EVENBOOKS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7d4b8a52-2f0e-4c39-9a51-0b6f0c2e9d13" \ -d '{ "version": 1 }'Response { "id": "4436acc5-82a4-4139-9f55-4988b6b2fc61", "number": "INV-0001", "status": "issued", "issuedAt": "2026-10-19T14:03:22.617Z", "balanceDue": "1250.00", "version": 2, … }From here: email it with
POST /invoices/{id}/send, record the customer’s payment withPOST /payments, or get told when it is paid with a webhook.
Authentication
Send an API key in the Authorization header of every request: Authorization: Bearer evb_…. Keys work only under /api/v1.
- One company per key. The key decides whose books you are working in, so you never send a company id. To work with several companies, create a key in each. A company can have 25 active keys.
- Role.
memberkeys do everyday bookkeeping: customers and vendors, products and services, invoices, credit notes, payments, expenses, bank transactions and bank statement imports, journal entries, reconciliation and reports.adminkeys can also do what only administrators can do in the app:- change company details and settings, invoice branding and the invoice email template, and send a test invoice email
- set up, check and choose the domain invoice email is sent from
- add and change bank accounts, and connect, link, reconnect or disconnect bank feeds
- add and change sales tax rates
- enter starting balances, and set the cash-basis amounts of imported journal entries
- choose the Schedule C lines for a tax year
- lock and reopen accounting periods
- import ledger history from another bookkeeping app (general ledger CSV)
- change online payment settings, refresh or disconnect Stripe, and refund card payments
- manage webhooks: endpoints, test events, the delivery log and signing secrets
- Access.
readkeys may makeGETrequests, plus the two calculation previews (POST /invoices/preview,POST /company/invoice-presentation/sample-pdf), which change nothing.writekeys may do everything their role allows. Use a read key for reporting, exports and dashboards. - Expiry and revocation. A key can expire on a date you choose, at most two years ahead, or never. Revoke a key in Settings → Developers; the next request with it gets
401. Expired and revoked keys stay listed, so you can see what they did. - History. Everything a key changes shows in History under the key’s name, like a person’s changes.
Keep keys on a server.
A write key can change your books. Store it in an environment variable or a secret manager. Never put it in a web page, a mobile app, a browser extension or a code repository: anything shipped to a device can be read. If a key leaks, revoke it and create a new one.
Base URL and versioning
Every path in this guide and in the reference is relative to https://evenbooks.org/api/v1. Requests and responses are JSON (Content-Type: application/json) unless an endpoint says it returns a PDF or CSV file.
Every response carries an api-version header with the date of the contract it follows (currently 2026-10-19) and an x-request-id. Include the request id when you write to us about a request.
What can change
Within v1, changes are additive: new endpoints, new optional request fields, new response fields and new webhook event types. Write clients that ignore fields and event types they don’t know. A change that would break a working client (removing or renaming a field, changing a type, tightening a rule) would come as /api/v2, announced in advance, with a deprecation period during which v1 keeps working. Operations on their way out are marked Deprecated in the reference.
Conventions
- Money
- Amounts are decimal strings, never floating-point numbers:
"1250.00","-40.25","2.5". Money totals and balances have the currency’s two decimal places. Quantities, unit prices and tax rates may have more, up to six ("0.125"); line amounts are rounded to the currency’s decimals. - Dates and times
- Calendar dates are
YYYY-MM-DD(2026-10-19), as the company sees them in its own time zone. Timestamps are ISO 8601 in UTC (2026-10-19T14:03:22.617Z). - Lists and paging
- Lists return
{ "items": […], "nextCursor": "…", "total": 120 }. Passlimitto choose the page size andcursor=<nextCursor>to get the next page;nextCursorisnullon the last page. Lists that count their rows includetotal. Each list’s limits are in its reference entry; a few short lists, such as the chart of accounts, return a plain array. - Versions and edits
- Records carry a
version. Changes (PATCH, issue, void and similar) take theversionyou last read. If the record changed since, the request fails with409 version_conflictand nothing is saved: read the record again, check it, and retry. - Idempotency
- Commands that change your books (issue, void, record a payment, categorize a bank transaction…) accept an idempotency key, either as
idempotencyKeyin the body or as anIdempotency-Keyheader, 8 to 128 characters. Use a new key, such as a UUID, for each action you intend and reuse it when you retry that action. A retry with the same key and the same body returns the first result instead of doing the work twice; the same key with a different body returns409 conflict. The reference marks these endpoints Idempotent.
Errors
Errors use HTTP status codes and one JSON shape. code is stable: switch on it. message is a sentence for people and its wording may change. details is there when there is more to say, and requestId matches the x-request-id header.
{
"error": {
"code": "period_locked",
"message": "Mar 31, 2026 is in a locked period. Reopen it in Settings → Accounting periods, or use a later date.",
"details": { "date": "2026-03-31" },
"requestId": "318634f8-8923-4f3d-93e5-098d8710dfd8"
}
}Validation errors list every problem, each with the path of the field:
{
"error": {
"code": "validation",
"message": "Some details are missing or not valid. Check the form and try again.",
"details": [
{ "code": "too_small", "path": ["lines"], "message": "Too small: expected array to have >=1 items" }
],
"requestId": "6f1c2a90-4b7e-4d8a-9a0e-2b5d7c1e3f48"
}
}| Code | Status | Meaning |
|---|---|---|
validation | 400 | A field is missing or not valid, or the body is not JSON. details lists each problem with its path. |
unbalanced | 400 | The amounts don’t balance: in a journal entry, debits and credits must add up to the same total. |
over_allocation | 400 | More would be applied than is available: more than a payment or credit holds, or more than an invoice still owes. |
unauthenticated | 401 | No API key, or the key is unknown, revoked or expired. |
forbidden | 403 | The key can’t do this: it is read-only, it needs the administrator role, or the endpoint is for signed-in people only. |
not_found | 404 | The record does not exist in this key’s company. Also returned when an API key is sent to a path outside /api/v1. |
conflict | 409 | The request conflicts with something already saved, for example an idempotency key reused with a different body. |
version_conflict | 409 | The record changed since you read it. Read it again and retry with its new version. |
period_locked | 409 | The date falls in a locked accounting period. Reopen the period or use a later date. |
immutable | 409 | Saved financial records can’t be edited. Void or reverse them instead. |
duplicate | 409 | A value that must be unique is already used (an invoice number, an account code, a name), or the file was already imported. |
invalid_state | 409 | The record’s status doesn’t allow this, for example issuing an invoice that is already void. |
company_mismatch | 409 | An x-company-id header names a different company than the key’s. Leave the header out. |
no_organization | 412 | Signed-in sessions only: no company is selected. API keys never get this. |
rate_limited | 429 | The key went over its request budget: wait Retry-After seconds, then try again. Sending invoice email can also answer this when the company has used its email quota for the hour or the day; that answer has no Retry-After. |
internal | 500 | Something failed on our side. Nothing was saved. Try again; if it keeps happening, send us the requestId. |
external_error | 502 | A service we rely on (Stripe, the bank feed, email) failed or refused. Try again later. |
Safe to retry as they are: 429 (after waiting), 500 and 502. With an idempotency key, retrying a command is always safe. Other errors need a change to the request first.
Rate limits
Each key may make 120 requests a minute and 10,000 a day (the day runs midnight to midnight UTC). The limits are the same on every plan. Every response says where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute. |
X-RateLimit-Remaining | Requests left in the current minute. |
X-RateLimit-Reset | Seconds until the minute window starts again. |
X-RateLimit-Limit-Day | Requests allowed per day. |
X-RateLimit-Remaining-Day | Requests left today. |
Over the limit, the API answers 429 with code rate_limited and a Retry-After header in seconds. Wait that long before trying again. If you need more, write to hello@evenbooks.org and say what you are building.
Invoice email quota
Sending invoice email (POST /invoices/{id}/send, the send action of POST /invoices/bulk and POST /company/invoice-presentation/test-email) also counts against the company’s email quota, shared with the app: 20 an hour and 50 a day on the free plan and 200 an hour and 1,000 a day on the paid plan, counted over the last hour and the last 24 hours. Over it, the email isn’t sent and the answer is 429 rate_limited without a Retry-After header (in a bulk request, that invoice’s result carries the code). details.window says hour or day; try again later.
Webhooks
Instead of asking the API for changes, let Evenbooks tell you. When something happens in the books, we send a signed POST to your endpoint.
Add an endpoint
An administrator adds endpoints in Settings → Developers, or calls POST /webhook-endpoints with an administrator key. Choose the events to receive, or leave events empty for all of them. A company can have 10 endpoints. URLs must use https:// and point to a public address; they can’t contain a user name, password or #fragment.
curl -X POST https://evenbooks.org/api/v1/webhook-endpoints \
-H "Authorization: Bearer $EVENBOOKS_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/evenbooks",
"description": "Order system",
"events": ["invoice.issued", "invoice.paid", "payment.created"]
}'{
"endpoint": {
"id": "46597786-9a55-4546-afa4-2e97a7cae03c",
"url": "https://example.com/hooks/evenbooks",
"events": ["invoice.issued", "invoice.paid", "payment.created"],
"status": "enabled",
"secretPrefix": "whsec_3K6B",
…
},
"secret": "whsec_…"
}The response includes the endpoint’s signing secret (whsec_…). It is shown once. Store it next to your API key. To replace it, use POST /webhook-endpoints/{id}/rotate-secret or Rotate signing secret in Settings; the new secret is also shown once.
Events
Event names follow the actions History records, so you hear about the same changes people see there.
Invoices
invoice.draft_createdinvoice.issuedinvoice.paidinvoice.voidedinvoice.email_sentinvoice.email_failedinvoice.deleted
Credit notes
credit_note.issuedcredit_note.appliedcredit_note.voidedcredit_note.deleted
Payments received
payment.createdpayment.allocatedpayment.allocation_reversedpayment.voidedpayment.deleted
Refunds
refund.createdrefund.voidedrefund.deleted
Expenses
purchase.createdpurchase.voidedpurchase.deleted
Journal entries
journal.postedjournal.reversedjournal_entry.deleted
Bank transactions
bank_transaction.createdbank_transaction.categorizedbank_transaction.matchedbank_transaction.transferbank_transaction.excludedbank_transaction.review_undonebank_transaction.deleted
Bank connections
bank_connection.syncedbank_connection.sync_failed
Imports
import.committed
Customers & vendors
contact.createdcontact.updatedcontact.archived
Products & services
item.createditem.updated
Recurring invoices
recurring.issued
Reconciliation
reconciliation.completed
Accounting periods
period.closedperiod.reopened
Online payments
processor.charge_settledprocessor.dispute_openedprocessor.payout_posted
What we send
The body is JSON. data.object is the record as the API returns it, read when the event was prepared, or null once the record is deleted. A record larger than 256 KB is left out: data.object is null and data.objectOmitted is "too_large", so fetch the record by data.entityId instead. actor says who made the change: a person (user), an API key (api_key, with the key’s id), or Evenbooks itself (system, job).
{
"id": "0b9f7c1e-5d0a-4f43-9a63-3c2b1f6d8e21",
"type": "invoice.issued",
"apiVersion": "2026-10-19",
"createdAt": "2026-10-19T14:03:22.689Z",
"companyId": "5f8077c8-1bae-4bc0-9183-2b10396edfb3",
"actor": { "type": "api_key", "id": "bb02cf19-f42a-49a9-a9b8-a2783b29fe40" },
"data": {
"entityType": "invoice",
"entityId": "4436acc5-82a4-4139-9f55-4988b6b2fc61",
"object": {
"id": "4436acc5-82a4-4139-9f55-4988b6b2fc61",
"number": "INV-0001",
"status": "issued",
"contactName": "Acme Supply Co.",
"total": "1250.00",
"balanceDue": "1250.00",
"version": 2,
…
}
}
}| Header | Value |
|---|---|
Evenbooks-Signature | Timestamp and signature: t=<unix seconds>,v1=<hex> |
Evenbooks-Event-Id | The event’s id; the same on every retry. |
Evenbooks-Event-Type | The event’s type. |
Evenbooks-Delivery-Id | This delivery to this endpoint, as shown in the delivery log. |
User-Agent | Evenbooks-Webhooks/1.0 (+https://evenbooks.org/developers) |
Content-Type | application/json |
Verify the signature
Before trusting a request, check that it came from us. v1 is the hex HMAC-SHA256 of <t>.<raw body> with your signing secret as the key. To verify:
- Read the body as raw bytes, before any JSON parsing. Parsing and re-serializing changes it, and the signature stops matching.
- Split
Evenbooks-Signatureon commas intotand one or morev1values. - Reject the request if
tis more than 5 minutes from your clock. This stops someone replaying an old request. - Compute the HMAC of
t, a period, and the raw body, and compare it with eachv1in constant time.
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 5 * 60;
/**
* True when `header` (the Evenbooks-Signature header) signs `rawBody`:
* the request body exactly as it arrived, before any JSON parsing.
*/
export function verifyEvenbooksSignature(secret, header, rawBody) {
if (typeof header !== 'string') return false;
let timestamp = null;
const signatures = []; // one or more v1= values
for (const piece of header.split(',')) {
const [key, value] = piece.trim().split('=', 2);
if (key === 't') timestamp = value;
else if (key === 'v1' && value) signatures.push(value);
}
// Unix seconds: ASCII digits only. Anything else is refused, never thrown.
if (!timestamp || !/^\d{1,12}$/.test(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
const expected = Buffer.from(createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex'));
return signatures.some((signature) => {
const given = Buffer.from(signature);
return given.length === expected.length && timingSafeEqual(given, expected);
});
}import { createServer } from 'node:http';
const secret = process.env.EVENBOOKS_WEBHOOK_SECRET; // whsec_…
const seen = new Set(); // keep processed event ids in your database instead
createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const rawBody = Buffer.concat(chunks).toString('utf8');
if (!verifyEvenbooksSignature(secret, req.headers['evenbooks-signature'] ?? '', rawBody)) {
res.writeHead(400).end('bad signature');
return;
}
const event = JSON.parse(rawBody);
if (!seen.has(event.id)) {
seen.add(event.id);
// Queue the work for event.type and event.data.object, then answer.
}
res.writeHead(200).end('ok');
});
}).listen(8080);With Express, mount express.raw({ type: 'application/json' }) on the webhook route so req.body is the raw Buffer.
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 5 * 60
def verify_evenbooks_signature(secret: str, header: str, raw_body: bytes) -> bool:
"""True when header (Evenbooks-Signature) signs raw_body, the bytes as they arrived."""
timestamp, signatures = None, [] # one or more v1= values
for piece in header.split(","):
key, _, value = piece.strip().partition("=")
if key == "t":
timestamp = value
elif key == "v1" and value:
signatures.append(value)
# Unix seconds: ASCII digits only (isdigit() alone also accepts "²" and "١٢٣").
if timestamp is None or not (timestamp.isascii() and timestamp.isdigit()) or len(timestamp) > 12:
return False
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
signed = timestamp.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected.encode(), s.encode()) for s in signatures)import os
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ["EVENBOOKS_WEBHOOK_SECRET"] # whsec_…
@app.post("/hooks/evenbooks")
def evenbooks_webhook():
signature = request.headers.get("Evenbooks-Signature", "")
if not verify_evenbooks_signature(SECRET, signature, request.get_data()):
return "bad signature", 400
event = request.get_json()
# Skip event["id"] if you have seen it; otherwise queue the work, then answer.
return "", 200Delivery, retries and timeouts
- Events arrive within about 5 minutes of the change.
- Answer with any
2xxstatus within 10 seconds. Do the work after answering, from a queue. A slow answer, an error status, a dropped connection or a redirect counts as a failure; we don’t follow redirects. - A failed delivery is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours: 8 attempts over about 2 days.
- Delivery is at least once, and a retried event can arrive after a later one. Skip events whose
idyou have already handled, and usecreatedAtor the record’sversionto tell which change is newer. When you need the current state, fetch the record. - Turning an endpoint off (
{ "enabled": false }) pauses it: deliveries already waiting stay waiting and are sent when you turn it back on. Events that happen while it is off are not recorded for it. - An endpoint whose every delivery fails for 3 days is turned off, and admins see why in Settings. So is an endpoint whose URL or signing secret was last set with an API key, when that key is revoked or expires. Either way the deliveries that were waiting are marked failed; once you turn it back on, in Settings or with
PATCH /webhook-endpoints/{id}and{ "enabled": true }, you can resend the ones you need. - The delivery log keeps 30 days of attempts, with the status code and the start of your response. Resend a delivery from the log or with
POST /webhook-deliveries/{id}/retry.
Test events
Send test event in Settings, or POST /webhook-endpoints/{id}/test, sends a ping event right away and shows what your endpoint answered. It is signed like any other event and changes nothing. You can’t subscribe to ping; every endpoint accepts it.
{
"type": "ping",
…
"data": {
"entityType": "webhook_endpoint",
"entityId": "46597786-9a55-4546-afa4-2e97a7cae03c",
"object": { "message": "Test event from Evenbooks. No records changed." }
}
}What API keys cannot do
A few things need a signed-in person. With an API key these endpoints answer 403 forbidden:
- Creating, listing and revoking API keys. Do that in Settings → Developers.
- Permanently deleting records. Voiding and reversing work with a key, and keep the history.
- Retrying a failed background job. Reading job status works.
- Connecting Stripe: onboarding and account linking happen in the browser. Once connected, charges and payouts can be read with a key.
- The browser session (
/session). UseGET /meinstead.
Team members, their roles and billing are managed in the app and have no API. A key can never reach another company’s books.
API reference
Generated from the code that answers each request, so it matches what the server does. Each area has its own page with its endpoints and the types they use. Paths are relative to https://evenbooks.org/api/v1. The same description is available as an OpenAPI 3.1 document for Postman, Insomnia or a client generator.
Administrator key marks endpoints that need a key with the admin role, Write access those a read-only key can’t call, and Idempotent those that accept an idempotency key.
Account
Who is calling and with what access.
Company
Business details and settings.
- getGet the company
- patchChange company details and settings
- getList email sender domains
- postSet up an email sender domain
- postCancel an email sender setup
- postCheck an email sender domain
- postChoose the invoice email sender
- getGet invoice branding and email template
- patchChange invoice branding and email template
- postPreview an invoice email
- postMake a sample invoice PDF
- postSend a test invoice email
Customers & vendors
People and businesses you sell to and buy from.
Products & services
Reusable invoice lines.
Invoices
Drafts, issuing, sending, PDFs and public links.
- getList invoices
- postCreate a draft invoice
- getGet an invoice
- patchChange an invoice
- DELDelete a draft invoice
- getList emails of an invoice
- postIssue an invoice
- getList PDF versions of an invoice
- postGet or remake the invoice PDF
- postGet, replace or turn off the public link
- postEmail an invoice
- postVoid an invoice
- postIssue or email many invoices
- postCalculate invoice totals
Credit notes
Credits against invoices.
Payments received
Money received from customers and how it is applied to invoices.
Refunds
Money returned to customers.
Recurring invoices
Invoices issued on a schedule.
Expenses
Money spent (purchases, bills paid, vendor refunds).
Banking
Bank accounts, connections, statement imports, bank transactions, rules and reconciliation.
- getList bank accounts
- postAdd a bank account
- patchChange a bank account
- getList bank connections
- postConnect a bank through SimpleFIN
- getGet a bank connection
- patchChange a bank connection
- postDisconnect a bank connection
- getList the accounts a bank connection can see
- postLink bank accounts to a connection
- postReconnect a bank connection
- postSync a bank connection now
- getList statement imports
- postPreview a CSV bank statement
- getGet a statement import
- postImport a previewed CSV statement
- postDiscard a CSV import
- postChange the columns of a CSV import
- getList reconciliations
- postStart a reconciliation
- getGet a reconciliation
- DELDelete a reconciliation
- postComplete a reconciliation
- postClear or unclear reconciliation lines
- postReopen a reconciliation
- getList categorization rules
- postCreate a categorization rule
- patchChange a categorization rule
- DELDelete a categorization rule
- getList bank transactions
- postAdd a bank transaction by hand
- getGet a bank transaction
- getList records a bank transaction could match
- postCategorize a bank transaction
- postExclude a bank transaction
- getList invoices a deposit could pay
- postMatch a bank transaction to recorded items
- postRecord a deposit as an invoice payment
- postRecord a bank transaction as a transfer
- postUndo the review of a bank transaction
- postReview many bank transactions at once
Journal entries
The ledger itself, including manual entries and starting balances.
Chart of accounts
Categories and balance sheet accounts.
Sales tax
Sales tax rates.
Tax form lines
Which Schedule C line each category reports on.
Accounting periods
Closing and reopening periods.
Reports
Financial reports as JSON, CSV or PDF.
- getGet the balance sheet
- getGet income and expenses by category
- getGet the general ledger for one account
- getGet unpaid invoices by age
- getGet the profit and loss report
- getGet the Schedule C summary for a tax year
- getList every ledger line in a date range
- getGet the trial balance
- getCount bank transactions not yet in reports
Imports
Importing history from another app.
Attachments
Receipts and other files attached to records.
Online payments
Card and bank payments through Stripe.
History
Every change, who made it and when.
Background jobs
Email, PDF, sync and recurring work the server does later.
Webhooks
Get notified when something changes.
Error status codes
Each endpoint lists the statuses it can return. They mean the same everywhere; the error codes in the body say exactly what went wrong.
- 400
- Validation failed (
validation,unbalanced,over_allocation). - 401
- No credential, or the API key is unknown, revoked or expired (
unauthenticated). - 403
- The key is read-only, lacks the administrator role, or the endpoint is for signed-in people only (
forbidden). - 404
- The record does not exist in this company (
not_found). - 409
- The record changed, is in the wrong state, or is in a closed period (
conflict,version_conflict,invalid_state,period_locked,immutable,duplicate). - 429
- Too many requests for this key (
rate_limited). WaitRetry-Afterseconds.