Skip to content

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.

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 failureSample
{
  "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
WhatWritten asExample
An amountinteger minor units + currency{ "minorUnits": 185000, "currency": "USD" }
A quantityinteger thousandths1.5 hours → 1500
A rate or discountinteger basis points8.25% → 825
A due datecalendar date in the business’s time zone2026-10-15
A momentISO 8601 timestamp2026-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 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: 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.

Added on 1 November 2026: an industry pack can ask a few setup questions before a business adopts it. GET /v1/business-types/{key}/setup-questions (MCP get_business_setup_questions, business:read) returns each question with its options and defaults, the words inside the envelope; with a businessId, the answers that business gave. The adoptBusinessType action takes the answers as modifiers, a question id to the option ids chosen, and sets up only what they name; an id the pack does not ask is refused. Nothing that existed changed.

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 →

Try it in your sandbox

A bmk_test_ key reaches only your sandbox workspace.

We use Google Analytics on our website by default to understand visits. Choose Decline to turn it off; you can change this at any time on the Cookies page. We never send Google your name, email or account. Cookie policy