The three other guides walk through flows; this one gathers the rules that apply to every call, to keep at hand while you write your client.
BASE=https://<deployment>.convex.site/api/v1
KEY=ba_...1. The data envelope
Every successful response wraps its payload in data:
{ "data": [ ... ] }Two bodiless exceptions: 204 No Content on deletes, and 302 on PDF, receipt and logo downloads, to follow with -L.
2. Pagination
Lists of company resources are paginated, newest first:
{ "data": [ ... ], "nextCursor": "..." }Three lists are unpaginated and answer a plain array under data: GET /companies, GET /mandates and GET /companies/{companyId}/invoice-templates.
3. Dates and ids
Dates are accepted as epoch milliseconds or YYYY-MM-DD, and returned as epoch milliseconds. The from/to filters bound the issue date, both ends included. One exception: absences store and return YYYY-MM-DD strings, because that is their sort order.
Ids are opaque strings. An unknown id, a malformed id and an id of another company all answer 404 NOT_FOUND: the three cases are deliberately indistinguishable, so that no id of another account leaks.
4. Strict bodies
- Bodies of the account, company, client, invoice, quote, recurring schedule and template routes are strict: any unknown key is rejected.
- A body is always required on
POSTandPATCH({}is a valid empty patch; an empty body isINVALID_JSON), except on bodiless actions such asPOST …/default;DELETEtakes none. - Over 1 MB, the body is refused:
413 PAYLOAD_TOO_LARGE. - Five routes take raw bytes rather than JSON: an expense receipt, the AI extraction and the AI attachment (PDF, PNG, JPEG or WebP, 10 MB), plus
PUT …/logoon a company and on an invoice template (PNG or JPEG only, 2 MB).Content-Typenames the format and the body is the file; everything else travels in the query string. - An unknown query key is a
400 VALIDATION_ERRORon every route that declares a query contract;GET /meandGET /companiesignore the rest, without that being a promise. PUTon an absence is a full replace: omitted optional fields are cleared.
A validation failure lists every offending field under issues, as dotted paths, array indexes included:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request body",
"issues": [
{ "path": "subtotal", "message": "Unrecognized key \"subtotal\"" },
{ "path": "vatRates.0.rate", "message": "Too big: expected number to be <=100" }
]
}
}5. Errors
Errors carry a stable, machine-readable code:
{ "error": { "code": "PAYSLIP_ALREADY_EXISTS" } }Only VALIDATION_ERROR (with issues) and INVALID_TRANSITION add a message; every other code answers alone. Switch on error.code, never on prose. This envelope holds on every path under /api/v1/, including an unknown path (404 NOT_FOUND) and an unsupported method (405 METHOD_NOT_ALLOWED, with an Allow header).
Checks run in this order: key and owner (401), company visibility (404), then archived companyplanmodulepermission (403). A company the key holder is not a member of is indistinguishable from a non-existent one.
401 INVALID_API_KEY: missing, malformed, unknown or revoked key; owner banned or deleted.403 PLAN_NOT_ALLOWED,403 COMPANY_ARCHIVED,403 ACCESS_DENIED: plan, archiving, missing permission or role.403 INVOICING_MODULE_DISABLED,403 PAYROLL_MODULE_DISABLED,403 EXPENSES_MODULE_DISABLED,403 FINANCIAL_STATEMENTS_MODULE_DISABLED: module off on the company.403 *_LIMIT_REACHED: plan quota reached (clients, invoices, recurring schedules, employees, payslips, expenses).400 BATCH_SIZE_INVALID: batch outside 1 to 100 items.409 INVALID_TRANSITION,409 PAYSLIP_ALREADY_EXISTS,409 ABSENCE_OVERLAPS_EXISTING,409 QUOTE_NOT_EDITABLE: conflict with the current state.422 MODIFICATION_REASON_REQUIRED,422 CLIENT_EMAIL_MISSING,422 PERIOD_FISCAL_YEAR_MISMATCH: valid request, inadmissible in this context.503 PDF_UNAVAILABLE,502 EMAIL_SEND_FAILED,500 INTERNAL_ERROR: retry.
The complete list of codes is the enum published in your deployment's Swagger UI and openapi.json.
6. Limits and usage
There is no rate limiting yet. Calls are counted, not throttled: one counter per key, endpoint, target company and UTC day, visible in AccountAPI keysUsage. Requests rejected by authentication are not counted; a 400 on an authorised call is.
Be a good neighbour anyway: page through lists rather than polling them, and back off on 500 or 503. The two batches do not behave alike: POST …/payslips/batch is not atomic and returns a result per item, POST …/bank-transactions/batch is atomic, all or nothing.
7. Sandbox and reset
The sandbox never writes to production. Emails, Stripe and Clerk invitations are inert there, sign-up is closed, logos are not copied. Everything else runs, including the scheduled jobs: recurring invoices are generated and payslips computed on the copied data.
Back to the flows: Getting started with the API, Invoicing with the API, Payroll, expenses and accounting with the API. Every field, every response and the complete enum of codes are in your deployment's Swagger UI (…/api/docs).