BillOS v1
Console llms.txt OpenAPI

Authentication

Every request carries your API key in the X-Api-Key header:

curl -H "X-Api-Key: bk_live_..." https://api.billos.co.il/v1/businesses/BID

A key is also accepted as Authorization: Bearer bk_..., for clients that only speak Bearer.

No key yet? POST https://api.billos.co.il/v1/sandboxes needs no credentials: it answers a link a person opens to approve, and the sandbox key arrives once they do (section 10). It is the start a coding agent can make by itself, with its user approving once.

> ocr is the one scope you are not given by default. Every receipt scan runs a billed AI call on our side (in the sandbox exactly as in live), so it is granted on request rather than bundled: ask from the console (מפתחות API → סריקת קבלות) and we enable it on both of your environments. Everything else is on your keys from the first minute.

Multi-tenant model

One API key owns many businesses. Each business is one Israeli dealer (עוסק), one of your end users. You create a business per user (section 4), and GET /businesses lists every business under your key; every other endpoint is nested under /businesses/:bid and only works for businesses your key created. A request for another key's business returns 404 (not 403: existence is not revealed).

Three credentials open this surface, and every route treats them the same way once they are in: an API key (X-Api-Key, yours, over the businesses you opened), a session (the console and the BillOS app, a person over their own businesses), and a Connect access token (Authorization: Bearer bkat_…, section 15: your app over ONE business whose owner connected it, with the scopes they consented to).

Every business row carries access: how the credential making the request holds it and with which scopes ({ via: "owner" | "shared" | "connect", owner, scopes: [...] }). Under your key it is owner with your key's scopes, always; the other two are how a person the owner invited, or an app the owner connected, sees the same business with less.

Key rotation

A client can hold several keys, up to a ceiling of 10 active keys at once (rotation headroom, not a fleet; configurable per account if you genuinely need more). Minting and revoking are self-service from the console: sign in at dash.billos.co.il and issue or kill a key yourself. A test key takes the sign-in and nothing else; a live key exists once your account is approved, and everything about one stands behind an SMS check. To rotate: mint the successor, switch your deployment to it, revoke the predecessor. Nothing else under the client moves. The same sign-in shows your environments in the console's business switcher (סביבת בדיקות / Live API, one group per key you hold) with the businesses each client opened, and a מפתחים section per environment: סקירה (a quickstart until the first calls, then traffic, top errors, webhook health and the keys), לוגים, שגיאות, אירועים, Webhooks (a page per endpoint with its deliveries) and מפתחות API. No key is pasted anywhere for that: the session is verified as the client's owner on every call.

GET /keys lists every key under your client with its prefix, label, creation time and last use, and marks the one making the request, so a key you did not create is visible to you.

Two things a key can never do to itself, by design: mint and revoke. Those live behind your console session (with an SMS check for a live key), because a leaked key that could issue its own successor would survive its own revocation. billos login (section 13) is the same rule from the terminal: the browser approves, the key is minted there.

Status

Live status of the API, the docs, the console and the dependencies behind them: status.billos.co.il (/status.json for machines). Incidents are posted there.

Pin a key to your servers

PATCH /keys/:id {"allowedCidrs": ["203.0.113.10", "198.51.100.0/24"]} restricts one of your keys to those source addresses (IPv4/IPv6, bare or CIDR, up to 20); a request from anywhere else answers 403 forbidden. A key copied out of a log is then worth nothing off your servers. An empty list clears it. A list that would lock out the very address you are calling from, on the key you are calling with, is refused, so you cannot cut yourself off by accident. GET /keys shows each key's list.

Rate limit

60 requests per minute per key (sliding window). Every response carries the budget: X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (a unix timestamp for when the oldest request leaves the window), so a client sees it draining instead of learning from the 429. Exceeding it returns 429 {"reason":"rate_limited"} with a Retry-After header (seconds). Back off and retry. GET /usage?days=30 returns your own per-endpoint request and error counts (usage) and the same window day by day (daily), the first place to look when you are throttled or failing.

Behind that sit a few daily backstops, deliberately high enough that a working integration never meets them and low enough to bound a runaway loop or a leaked key. They apply to every account on every plan, and they answer limit_reached (429):

BackstopDefault
Documents opened per business, per day300
Expense rows opened per business, per day (uploads included)300
New businesses per API key, per day50
OCR scans per account, per day200 (quota_exceeded): a developer account's sandbox and live clients share one count; a partner client we minted counts alone

These are not your plan's ceilings; those are the monthly numbers on the business object (section 4). If a legitimate workload needs more of either, tell us and we raise it on your account.