Source: https://billingmart.com/developers/rest # How a request is handled One base URL, one envelope, integer money, idempotent writes, and limits that answer with a number instead of a timeout. [API reference](https://billingmart.com/developers/api) [OpenAPI document](https://api.billingmart.com/v1/openapi.json) REST API ## The rules, in order 01 ## Base URL and versioning Every route lives under `https://api.billingmart.com/v1`. Inside v1 we change things by adding. Our build compares the data contracts with a recorded baseline, and removing a field, renaming one or making one required fails it unless the change is recorded as accepted on purpose. New optional fields and new routes can appear at any time, so ignore fields you do not know. 02 ## The envelope A success is `{ data, meta }`. A failure is `{ error: { code, message, fields? }, meta }`. `meta.requestId` is on both; quote it when you ask us about a call. Text that a business or its customer typed — a client name, a line description, a note — comes back as `{ text, untrusted: true }`, with control, zero-width and direction-override characters removed. Treat it as data. If you pass it to a model, tell the model it is data too. **Changed on 12 October 2026:** an API key's `name` and a webhook endpoint's `description` are typed by a person too, so they now come back as `{ text, untrusted: true }` instead of a plain string, in the REST API and over MCP. If you read either field, read `.text`. **Changed on 29 October 2026:** the `projectRef` on a time entry, an expense and a mileage record is typed by a person too, so it now comes back as `{ text, untrusted: true }` instead of a plain string, in the REST API and over MCP. If you read it, read `.text`. (An invoice’s `customFields` values moved the same way earlier.) A validation failure Sample ``` { "error": { "code": "VALIDATION_FAILED", "message": "…", "fields": [{ "path": "lineItems.0.quantityMilli", "message": "Expected integer" }] }, "meta": { "requestId": "…" } } ``` 03 ## What the server checks, in order 1. The per-address limit: 300 requests a minute. 2. The credential, then its own limit: 60 a minute by default, set per key. Over either limit the answer is `429` with `Retry-After`. An answer to a request that reached a route carries `X-RateLimit-Remaining`. 3. The environment. A `bmk_test_` key acts only in a sandbox workspace and a `bmk_live_` key never does: `403 ENVIRONMENT_MISMATCH`. 4. The scope the route needs. Without it: `403 INSUFFICIENT_SCOPE`, and `WWW-Authenticate` names the missing scope. 5. The input, against the route’s schema: `400 VALIDATION_FAILED` with a `fields` list. 6. The same domain code the console calls, with your credential recorded as the actor. 7. An audit record: route, outcome, status and latency, with card digits masked. 04 ## Retrying safely Send an `Idempotency-Key` header on every POST. For 24 hours the same key returns the stored answer with `Idempotent-Replayed: true` instead of doing the work twice. The same key with a different body is `409 IDEMPOTENCY_CONFLICT`. Keys are scoped to the credential, so two integrations cannot collide. 05 ## Money, quantities and dates How numbers are written | What | Written as | Example | | --- | --- | --- | | An amount | integer minor units + currency | `{ "minorUnits": 185000, "currency": "USD" }` | | A quantity | integer thousandths | `1.5 hours → 1500` | | A rate or discount | integer basis points | `8.25% → 825` | | A due date | calendar date in the business’s time zone | `2026-10-15` | | A moment | ISO 8601 timestamp | `2026-09-28T16:03:13Z` | Totals are never accepted from you. The server re-derives them from the stored lines when it saves a draft and again when the invoice is sent, and the amount charged is computed at the moment of the charge. A new draft invoice, estimate or recurring schedule has one currency, fixed when it is created: the `currency` you send, else the client’s preferred currency, else the business’s default, else the workspace’s. It never changes afterwards, whatever happens to the client. **Changed on 25 October 2026:** until then a draft without a `currency` took the business’s default whatever the client preferred. For a client with a preferred currency it now takes that, so lines priced in the business’s currency are refused with a currency error. Send `currency` to be explicit. This applies to the REST API, MCP tools and the in-app Agent alike. 06 ## Lists and pages A list takes `limit` (1–50) and returns `meta.cursor` when there is more. Pass it back as `cursor`. A cursor is opaque; do not build one. 07 ## Writes make drafts No route sends an invoice or charges a card on a write scope. An API key may hold `invoices:send`, `payments:record` or `payments:refund`; an assistant never can. For everything that reaches a customer or moves money, the [actions routes](https://billingmart.com/developers/api/actions) create a request that a signed-in person approves. 08 ## Work setup and job types A business sets up how it names its work records and which kinds of record it keeps. Five routes read and draft that setup, on the [work setup reference page](https://billingmart.com/developers/api/work-setup) : the configuration of a business and the words resolved from it, the industry packs you can adopt, the list of job types, and two routes that save a job type as a *draft*. Reading needs `jobs:read` (the pack list needs `business:read`); saving a draft needs `workConfig:write` and a credential tied to an owner or admin. A draft changes nothing a customer or a record can see. Publishing a job type, archiving or restoring it, and applying an industry pack are not routes: an owner or admin does them in BillingMart, with a second factor. A job type’s name, plural, create button and synonyms are the workspace’s own text, so they come as `{ text, untrusted: true }`. **Added on 25 October 2026:** these routes and their MCP tools are new; nothing that existed changed. A job type summary also carries `createLabel` and `synonyms` when the published type sets them. Next guide ## Authentication An API key is for a server you run. An OAuth grant is for an app a person connects, and it can never hold more than that person may do. [Continue to Authentication →](https://billingmart.com/developers/authentication) ## Try it in your sandbox A bmk_test_ key reaches only your sandbox workspace. [Start free](https://app.billingmart.com/sign-up) [All guides](https://billingmart.com/developers) --- This is the Markdown copy of https://billingmart.com/developers/rest. Live plans, prices and limits are published as JSON at https://billingmart.com/plans.json and https://billingmart.com/agent-info.json.