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 the api-version header
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.

Quick start

From a new key to an issued invoice in five requests. The examples use curl and, in one place, jq.

  1. 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.

  2. Step 2: Check that it works

    GET /me tells 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 }
    }
  3. Step 3: Find the income category

    Every invoice line says which income category it counts toward. GET /accounts returns the chart of accounts as a list; keep the id of an income account 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" }
    …
  4. Step 4: Add a customer

    POST /contacts with a name. isCustomer is true unless you say otherwise; set isVendor for 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,
      …
    }
  5. Step 5: Create a draft invoice

    POST /invoices needs the customer, an issue date and at least one line. Give termsDays or a dueDate; 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}.

  6. Step 6: Issue it

    POST /invoices/{id}/issue with the draft’s version. Issuing gives the invoice its number and records it in your books. Send an Idempotency-Key so 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 with POST /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. member keys 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. admin keys 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
    The reference marks those endpoints Administrator key.
  • Access. read keys may make GET requests, plus the two calculation previews (POST /invoices/preview, POST /company/invoice-presentation/sample-pdf), which change nothing. write keys 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 }. Pass limit to choose the page size and cursor=<nextCursor> to get the next page; nextCursor is null on the last page. Lists that count their rows include total. 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 the version you last read. If the record changed since, the request fails with 409 version_conflict and 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 idempotencyKey in the body or as an Idempotency-Key header, 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 returns 409 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.

409 Conflict
{
  "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:

400 Bad Request
{
  "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"
  }
}
CodeStatusMeaning
validation400A field is missing or not valid, or the body is not JSON. details lists each problem with its path.
unbalanced400The amounts don’t balance: in a journal entry, debits and credits must add up to the same total.
over_allocation400More would be applied than is available: more than a payment or credit holds, or more than an invoice still owes.
unauthenticated401No API key, or the key is unknown, revoked or expired.
forbidden403The key can’t do this: it is read-only, it needs the administrator role, or the endpoint is for signed-in people only.
not_found404The record does not exist in this key’s company. Also returned when an API key is sent to a path outside /api/v1.
conflict409The request conflicts with something already saved, for example an idempotency key reused with a different body.
version_conflict409The record changed since you read it. Read it again and retry with its new version.
period_locked409The date falls in a locked accounting period. Reopen the period or use a later date.
immutable409Saved financial records can’t be edited. Void or reverse them instead.
duplicate409A value that must be unique is already used (an invoice number, an account code, a name), or the file was already imported.
invalid_state409The record’s status doesn’t allow this, for example issuing an invoice that is already void.
company_mismatch409An x-company-id header names a different company than the key’s. Leave the header out.
no_organization412Signed-in sessions only: no company is selected. API keys never get this.
rate_limited429The 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.
internal500Something failed on our side. Nothing was saved. Try again; if it keeps happening, send us the requestId.
external_error502A 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:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute.
X-RateLimit-RemainingRequests left in the current minute.
X-RateLimit-ResetSeconds until the minute window starts again.
X-RateLimit-Limit-DayRequests allowed per day.
X-RateLimit-Remaining-DayRequests 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.

Request
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"]
  }'
Response
{
  "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_created
  • invoice.issued
  • invoice.paid
  • invoice.voided
  • invoice.email_sent
  • invoice.email_failed
  • invoice.deleted

Credit notes

  • credit_note.issued
  • credit_note.applied
  • credit_note.voided
  • credit_note.deleted

Payments received

  • payment.created
  • payment.allocated
  • payment.allocation_reversed
  • payment.voided
  • payment.deleted

Refunds

  • refund.created
  • refund.voided
  • refund.deleted

Expenses

  • purchase.created
  • purchase.voided
  • purchase.deleted

Journal entries

  • journal.posted
  • journal.reversed
  • journal_entry.deleted

Bank transactions

  • bank_transaction.created
  • bank_transaction.categorized
  • bank_transaction.matched
  • bank_transaction.transfer
  • bank_transaction.excluded
  • bank_transaction.review_undone
  • bank_transaction.deleted

Bank connections

  • bank_connection.synced
  • bank_connection.sync_failed

Imports

  • import.committed

Customers & vendors

  • contact.created
  • contact.updated
  • contact.archived

Products & services

  • item.created
  • item.updated

Recurring invoices

  • recurring.issued

Reconciliation

  • reconciliation.completed

Accounting periods

  • period.closed
  • period.reopened

Online payments

  • processor.charge_settled
  • processor.dispute_opened
  • processor.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).

Body
{
  "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,
      …
    }
  }
}
HeaderValue
Evenbooks-SignatureTimestamp and signature: t=<unix seconds>,v1=<hex>
Evenbooks-Event-IdThe event’s id; the same on every retry.
Evenbooks-Event-TypeThe event’s type.
Evenbooks-Delivery-IdThis delivery to this endpoint, as shown in the delivery log.
User-AgentEvenbooks-Webhooks/1.0 (+https://evenbooks.org/developers)
Content-Typeapplication/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:

  1. Read the body as raw bytes, before any JSON parsing. Parsing and re-serializing changes it, and the signature stops matching.
  2. Split Evenbooks-Signature on commas into t and one or more v1 values.
  3. Reject the request if t is more than 5 minutes from your clock. This stops someone replaying an old request.
  4. Compute the HMAC of t, a period, and the raw body, and compare it with each v1 in constant time.
Node.js (node:crypto)
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);
  });
}
Node.js: a receiver
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.

Python (hmac)
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)
Python: a Flask receiver
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 "", 200

Delivery, retries and timeouts

  • Events arrive within about 5 minutes of the change.
  • Answer with any 2xx status 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 id you have already handled, and use createdAt or the record’s version to 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.

ping
{
  "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). Use GET /me instead.

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.

Customers & vendors

People and businesses you sell to and buy from.

Products & services

Reusable invoice lines.

Invoices

Drafts, issuing, sending, PDFs and public links.

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.

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.

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). Wait Retry-After seconds.