BillOS v1
Console llms.txt OpenAPI

Businesses

Create a business: POST /businesses

One per end user, at their onboarding. taxId is their real 9-digit ע.מ/ת.ז, checksum-validated, so collect it carefully.

curl -X POST "$BASE/businesses" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{
  "name": "סטודיו איפור נועה",
  "taxId": "123456782",
  "dealerType": "patur",
  "address": { "street": "הרצל", "house": "12", "city": "תל אביב", "zip": "6688312" },
  "phone": "0501234567",
  "email": "noa@example.com"
}'
FieldRequiredNotes
name✓Registered business name (appears on documents)
taxId✓9 digits, Israeli checksum validated
dealerType✓"patur" | "murshe" | "company" | "ngo"
registrationNumber-ח"פ for companies: 9 digits, check digit validated (it prints in the מבנה אחיד header, where a wrong digit fails the ITA structure check). Send it for a company even when it is the same number as taxId. A company created without it prints its taxId under the ח.פ heading on documents, and its מבנה אחיד header carries no company number
address-{street, house, city, zip}
phone, email-
startNumbers-Numbering continuation for users migrating from another system: {"320": 1071} → their first invoice/receipt here is #1071

201 → {"ok":true, "business":{ "id": "...", ... }}. Store business.id; every other call needs it. A per-business signing certificate is issued automatically the first time a document PDF is rendered.

Pricing, the open-business limit and the card

A live business your key opens is billed ₪39 for every month it is open at some moment of, with no limit on documents; the month it was opened is billed only for the days left in it, from its opening day; the sandbox is free. The month is charged on the 1st to the card on your developer account (the console, שימוש וחיוב), where every invoice and its receipt are listed. A business opened and deactivated within 24 hours with nothing issued is not billed, and deactivating a business stops its billing from the next month without touching its books.

You decide how many businesses your key may hold open, in the console (1 to 500; 5 until you change it). Opening one more answers business_limit (403); so does reactivating one past the limit. With no card on file, or with a month left unpaid, opening a live business answers payment_required (402). Neither ever stops a business you already hold: issuing, expenses and reads keep working.

Read / update: GET /businesses/:bid · PATCH /businesses/:bid

PATCH updates profile fields (name, address, contact). taxId and dealerType are immutable once the business has issued records.

Account standing: POST /businesses/:bid/deactivate · POST /businesses/:bid/reactivate

Two separate switches govern what a business may write, and only one of them is yours:

Beside them sit the monthly ceilings: maxDocsPerMonth and maxExpensesPerMonth on the business object (null = unlimited). At a ceiling the write answers limit_reached (429) until the next calendar month (Asia/Jerusalem). A ceiling can come from the business's plan (planKey, a named tier) or from an override we set on the account itself; an override always wins, and the numbers on the business object are the ones actually in force.

Every one of these fields rides the business object, so GET /businesses/:bid always tells you why a write is being refused, and business.status_changed (section 11) pushes the change to you the moment it happens, so your users hear it from you rather than from a 403.

Shared access: GET · POST /businesses/:bid/access · DELETE /businesses/:bid/access/:id

Every dealer hands the books to an accountant, and until now that meant exporting files every month. The owner of a business can instead open it to a person, by email and role; the person signs in to the console with that address and finds the business in their switcher, with exactly the scopes the owner named. Nothing is typed twice: you name the address, they sign in with Google.

curl -X POST "$BASE/businesses/$BID/access" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"email":"rina@firm.co.il","role":"accountant","label":"משרד רו״ח כהן"}'
# 201 → { access: { id, email, role, scopes, status: "pending", ... }, emailed: true }
RoleWhat it holds
accountant (default)businesses:read, documents:read, expenses:read, export: everything to read and reconcile, plus generating the מבנה אחיד export, the VAT return, the reports and a backup
bookkeeperthe same, plus expenses:write: records expenses in the books
viewerthe three read scopes only

An explicit scopes list (fine scopes; ocr is never shared) overrides the preset, so a secretary who issues receipts is ["businesses:read", "documents:read", "documents:write"]. A person holding a business this way is not its owner: they cannot invite others, connect apps, or move the business, and the audit trail records what they did under their own identity. status is pending until the address signs in once, then active. The invitation email goes out on a new invite and again whenever a pending address is re-invited (resent: true), which is how a lost invitation is sent a second time; re-inviting an active address only updates its scopes and answers 200 with deduped: true and no letter. emailReason says what happened to it: sent, not_needed (a scope change), failed, unconfigured, or sandbox (a test key never mails anyone), so a sandbox invite opens the access and sends nothing. DELETE revokes on the spot (the person loses the business on their next request) and keeps the row as history. Up to 20 people per business. The owner-only routes answer 404 to anyone else, the same way another key's business does.

What the person sees is the same console you do (dash.billos.co.il): the business appears under "משותפים איתי", the buttons the grant does not cover are gone, and a POST the page never offered would still answer 403 missing scope: documents:write. The same holds on the MCP endpoint: an agent driven with an accountant's session can list and summarize but cannot issue.

The accountant's desk: GET /accountant/desk

An accountant with fifteen clients does not open fifteen businesses to learn which of them needs him. GET /accountant/desk?period=2026-09 answers with one row per business the credential was let into (period is a month, a bi-month 2026-B4 or a year; the default is the month that just closed). Each row carries the period's documents (count and income), expenses (recorded count and total, and drafts: receipts still waiting on their owner), vat (the return's figures and how many problems it would carry; null for a עוסק פטור), receivables (open and past due), lastActivityAt, a status (attention, ready or quiet) and the flags behind it (allocation_missing, vat_warnings, expense_drafts, overdue_debt, nothing_in_period). Rows needing attention come first. Money is agorot. Businesses where the credential cannot read both documents and expenses are left off.

Each row also carries vatFrequency (monthly, bimonthly or none), vatFrequencyAssumed and vatDeadline (period, due, detailedDue, daysLeft), the next VAT deadline from the Tax Authority's published calendar; the answer's calendarEnds is the last date that calendar knows, and past it no deadline is drawn. Only the accountant knows how often a client files, so PUT /accountant/desk/businesses/:bid {"vatFrequency": "bimonthly"} records it on the access row he holds (his alone; an owner or a key answers 403). Until he says, a VAT dealer is assumed monthly and a עוסק פטור none.

An accountant brings a client with POST /accountant/invite-link {"label": "משרד כהן"}: it answers a signed link (valid 30 days) carrying his verified email and the office name. The client opens it, GET /accountant/invite-link/preview?t= tells his screen who is asking, and his approval is an ordinary POST /businesses/:bid/access with role accountant. The link grants nothing by itself.

The periodic report: GET · PUT · DELETE /businesses/:bid/accountant-report · POST .../send

Shared access is for the accountant who signs in. This is for the one who never will: name an address, and at the start of each period BillOS mails him the books of the period that just closed, on its own.

curl -X PUT "$BASE/businesses/$BID/accountant-report" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"emails":["cpa@firm.co.il"],"frequency":"monthly","sendOnDay":5,"includePcn874":true}'
# 200 → { report: { emails: [...], frequency: "monthly", nextSendOn: "2026-10-05", lastPeriod: "2026-08", ... } }

The letter carries the period's figures in its body (income, recognised expenses, VAT to pay or refund, profit) and attaches a three-tab workbook: הכנסות (every issued document, each row linking to the signed PDF), הוצאות (the recorded rows with their receipts) and רווח והפסד, the same cash-basis numbers §7's reports print. With includePcn874 the detailed VAT return file (§10) rides along, ready to file.

FrequencyThe periodMailed on
monthlythe calendar month that closedsendOnDay of the following month, ahead of the 15th the return is due
bimonthlythe Jan-Feb … Nov-Dec pair that closedsendOnDay of the month after the pair
yearlythe calendar year that closedsendOnDay of January

A עוסק פטור files no VAT return, so his frequency is yearly and no PCN874 is attached; asking for anything else is refused. sendOnDay is 1..28, so every month has it. Saving never mails a backlog: the period already closed is stamped as sent, because a settings call should not post last month's books to someone the second it returns. POST .../send is how you send one deliberately: { "period": "2026-08" } for a particular one, the last closed period by default, and a period already mailed is skipped with sent: false, reason: "already_sent" unless force. Every send is in the business's audit trail, and a failure is kept on the row as lastError rather than retried into an inbox. Owner only, like every other decision about where this business's data goes.

Bank account on documents: PATCH /businesses/:bid with bankDetails

The account customers pay into by bank transfer. While onDocuments is true it prints under the totals of a חשבון עסקה (300) and a חשבונית מס (305), the documents that ask to be paid; never on a receipt, an invoice-receipt or a credit note. It is frozen into each document at issue, so changing the account later never alters a document already issued. A known Israeli bank, written by name in any of its usual spellings or by its Bank of Israel code, prints with the code: "לאומי" and "10" both print as "בנק לאומי (10)"; any other name prints as written.

curl -X PATCH https://api.billos.co.il/v1/businesses/$BID \
  -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"bankDetails": {"bankName": "לאומי", "branch": "800", "account": "123456", "beneficiary": "הדברות ירוקות בע\"מ", "onDocuments": true}}'

Each field is at most 80 characters; an unknown field is refused. "bankDetails": null clears it. It is returned on GET /businesses/:bid.

PDF branding: PATCH /businesses/:bid with branding

Per-business styling applied to every document PDF issued from then on (existing documents keep the look they were issued with, because the branding is frozen into each document's snapshot), and to the management-report PDFs (§7), which render live and so always reflect the current branding:

> The logo is frozen by reference. You send it as a data-URI, but an issued document's snapshot stores branding.logoRef ({fileId, sha256, mime}) rather than the image itself, so a snapshot stays a few kilobytes instead of a few hundred. The bytes are pinned by that hash and the rendered PDF is unaffected. If you read a snapshot back (GET /documents/:id) and want the image, fetch the file by its id; documents issued before this change still carry an inline branding.logo.

curl -X PATCH "$BASE/businesses/$BID" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{
  "branding": {
    "logo": "data:image/png;base64,iVBOR...",
    "tagline": "איפור כלות וערב",
    "accentColor": "#7A1FA2",
    "footerText": "תודה שבחרתם בנו! לקביעת תור: 052-1111111",
    "template": "stripe"
  }
}'
FieldRules
logo`data:image/pngjpegwebp;base64,...` URI, decoded ≤ 400KB. Rendered ~92px square at the document header
tagline≤ 120 chars, shown under the business name in the accent color
accentColor#rrggbb, colors the tagline, divider, מקור/העתק badge and table headers (default green #1F6B4A)
footerText≤ 240 chars, centered line above the footer (thank-you note, booking phone, etc.)
website≤ 200 chars, printed as a link in the business block
templateThe page design: classic (the default: the business header, the accent-colored table head, the digital-signature stamp) or stripe (the minimal layout of a Stripe receipt, mirrored to Hebrew: a plain title, a label/value grid, one headline amount, hairline tables, no fills; the accent color is spent on links only). Omit or null for classic

Both designs carry everything the tax rules require (מקור/העתק marking, the VAT ladder, the allocation number, the "מסמך ממוחשב" signature footer), so pick by taste. Both are light: a signed one-page document is roughly 75KB in the classic design and 50KB in the minimal one, and a two-page minimal document about 85KB.

Patch semantics per field: send a key to set it, null to remove it, omit to keep it. "branding": null clears everything. A per-issue {"branding": {...}} on the issue call overrides the stored defaults for that document only.

Branding themes: several looks for one business, /businesses/:bid/branding-themes

A business that sells under more than one brand (event halls and offices, a second line of work) keeps one ledger and one numbering, and prints each document in the look that fits it. The branding above is the main design; a theme is a whole alternative to it, with the same fields, plus an optional bank account, phone and email of its own to print (null prints the business's). A business keeps up to 20 live themes.

# a copy of the main design with another colour, design and account ("from": "main"; branding is then a patch over the copy)
curl -X POST "$BASE/businesses/$BID/branding-themes" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{
  "name": "אירועים",
  "from": "main",
  "branding": { "accentColor": "#8B1E3F", "template": "stripe", "tagline": "הדברה לאולמות ואירועים" },
  "bankDetails": { "bankName": "הפועלים", "branch": "600", "account": "123456", "onDocuments": true },
  "phone": "03-5559999",
  "email": "events@example.co.il"
}'
# 201 → {"ok":true,"theme":{"id":"…","name":"אירועים","branding":{…},"bankDetails":{…},"archived":false}}

# which theme a customer's documents print in by default
curl -X PATCH "$BASE/businesses/$BID/parties/$PARTY" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{ "defaultThemeId": "<theme id>" }'

# or name it on one document (also on a quote and a recurring plan)
curl -X POST "$BASE/businesses/$BID/documents" … -d '{ "docType": 305, "brandingThemeId": "<theme id>", … }'

Which theme a document prints in, in order: the brandingThemeId on the document, else its customer's defaultThemeId, else the main design. "brandingThemeId": "main" prints the main design whatever the customer's default. The choice is frozen at issue: the issued document's brandingThemeId names the theme it used (null for the main design) and its snapshot carries theme: { id, name }, so editing or archiving a theme later never changes a document already issued. A credit note prints in the look of the document it corrects, an invoice opened from a quote in the quote's, and every run of a recurring plan in the plan's.

A theme never changes who issues. The business's name, tax id and address head every document whatever theme it prints in: a tax invoice must state the dealer's name, address and number, and a document under another legal name belongs to another business, with its own numbering and its own מבנה אחיד. A trading name belongs in the theme's tagline; the phone, email, website and bank account may differ.

PATCH /businesses/:bid/branding-themes/:tid takes name, branding, bankDetails, phone and email with the same patch semantics as the main design, and archived: true | false. Themes are archived, never deleted: an archived theme is in no list (?archived=1 shows it) and cannot be chosen, and where it was a customer's default the main design prints, with branding_theme_archived in the issue's warnings.