BillOS v1
Console llms.txt OpenAPI

Documents

A document may name an ad-hoc party inline (just {"name": "..."}), which is fine for walk-ins. For repeat customers create a party once and reference it, so the customer ledger stays coherent:

curl -X POST "$BASE/businesses/$BID/parties" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -d '{ "kind": "customer", "name": "דנה לוי", "phone": "0521111111", "email": "dana@example.com" }'
# → {"ok":true,"party":{"id":"...","key":"C00001",...}}

# list / update / record consent to digital documents:
curl -H "X-Api-Key: $KEY" "$BASE/businesses/$BID/parties?kind=customer"
curl -X PATCH "$BASE/businesses/$BID/parties/$PARTY_ID" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{"phone":"0529999999"}'
curl -X POST "$BASE/businesses/$BID/parties/$PARTY_ID/consent" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{"via":"checkout-checkbox"}'

kind: "customer" | "supplier" | "both". The consent endpoint timestamps the party's agreement to receive documents digitally (מסמכים ממוחשבים). Record it once per customer if you deliver PDFs instead of paper.

5.2 Create a draft: POST /businesses/:bid/documents

curl -X POST "$BASE/businesses/$BID/documents" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-8812" -d '{
  "docType": 320,
  "party": { "name": "דנה לוי", "phone": "0521111111" },
  "priceMode": "gross",
  "lines": [
    { "description": "איפור כלה", "quantity": 1, "unitPriceExVat": 120000 },
    { "description": "איפור מלווה", "quantity": 2, "unitPriceExVat": 35000 }
  ],
  "payments": [ { "method": 3, "amount": 190000 } ]
}'
# 201 → {"ok":true,"document":{"id":"...","status":"draft","docNumber":null,...}}
FieldNotes
docType300 / 305 / 320 / 400 (330 only via the credit-note endpoint)
party{partyId} to reference a saved party, or inline {name, taxId?, phone?, email?, address?}; omit entirely for an anonymous cash receipt
linesRequired for 300/305/320: {description, quantity, unitPriceExVat}. Optional per line: sku, unit (default "יחידה"), lineDiscount (agorot), vatRateBp: 0 to make that one line VAT-exempt on an otherwise VAT-bearing document (mixed rates: VAT is then computed per line and summed, the totals carry vatRateBp: null and vatByRate, and a document-level discountAmount is refused; use line discounts)
priceMode"net" (the default: line prices are ex-VAT) or "gross": the same unitPriceExVat/lineDiscount fields carry VAT-inclusive amounts, the document total anchors to the gross sum and VAT is derived. Gross is what a consumer-facing price list wants: in the example above the customer pays exactly ₪1,900.00
paymentsRequired for 320/400: {method, amount} + optional {dueDate, bankCode, branchCode, accountNumber, checkNumber, cardCompany, cardName, cardTxType}
noteFree text (≤500 chars) printed on the document under the tables: remark, service context, terms. A credit note automatically carries its reason here
documentDate, valueDateDefault today (Asia/Jerusalem)
discountAmount, withholdingAmountDocument-level, agorot. Payments must cover the total net of withholding
currencyILS (default) or USD / EUR / GBP / CHF / AUD / CAD. A foreign currency makes every amount in the body (lines, discounts, payments) that currency's minor unit, cents instead of agorot. See "Foreign currency" below
fxRateILS per 1 unit of currency, your own rate. Omit to use the Bank of Israel שער יציג of the document date, fetched at issue. fxRateDate says which day yours is from
zeroVattrue states מע"מ בשיעור אפס (סעיף 30(א) לחוק מע"מ: a service to a foreign resident, export) on a 305/320: lines carry 0% VAT and the document prints the legal basis. Your claim, not checked by BillOS. Ignored on a receipt or for a עוסק פטור
langhe (default) or en: an English, left-to-right document for a foreign customer. Labels, the headline sentence, dates (September 4, 2026) and the email that carries the link are English. The document is bilingual by design, not English-only: the statutory headings (the document type, עוסק מורשה / ח.פ / עוסק פטור, מקור/העתק, מסמך ממוחשב, the signature line) stay in Hebrew beside their English, because an English-only original requires a written permit from the assessing officer and a bilingual one does not. Your line descriptions and note print as you send them, so write those in English too
externalRefIdempotency (or the Idempotency-Key header)

Payment method accepts a code or a string alias:

CodeAliasLabel on the PDF
1"cash"מזומן
2"check" / "cheque"המחאה
3"card" / "credit"כרטיס אשראי
4"transfer" / "bank_transfer"העברה בנקאית
5"voucher"תווי קניה
8"standing_order"הוראת קבע
9"other"אחר
91"bit"ביט
92"paybox"פייבוקס
93"payment_app" / "app"אפליקציית תשלום

Codes 91-93 are BillOS extensions so the printed document names the actual payment app; in the regulatory OPENFRMT export they are reported as 9 (אחר) per the ITA spec. Unknown values fall back to 9.

Foreign currency (מט"ח)

Israeli books are kept in shekels: סעיף 22 להוראות ניהול פנקסי חשבונות requires the record in Israeli currency and, when the payment came in another one, the currency and the amount paid in it beside the shekel figure. That is exactly what a foreign-currency document is in BillOS:

curl -X POST "$BASE/businesses/$BID/documents" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{
  "docType": 320, "currency": "USD", "lang": "en", "zeroVat": true,
  "party": { "name": "Acme Inc.", "address": "Austin, TX" },
  "lines": [ { "description": "Consulting, September", "quantity": 10, "unitPriceExVat": 15000 } ],
  "payments": [ { "method": 4, "amount": 150000 } ]
}'
# → a $1,500.00 document; at issue: fxRate 3.005 (BoI, 2026-09-04), totalIncVat 450750 agorot, fxTotalIncVat 150000

Drafts are freely mutable: they are a "קובץ זמני", not a tax document; any rendering of a draft is watermarked "טיוטה" and carries no number:

curl -X PATCH  "$BASE/businesses/$BID/documents/$DOC_ID" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{"lines":[...]}'
curl -X DELETE "$BASE/businesses/$BID/documents/$DOC_ID" -H "X-Api-Key: $KEY"

5.3 List / read

curl -H "X-Api-Key: $KEY" "$BASE/businesses/$BID/documents?status=issued&docType=320&from=2026-07-01&to=2026-07-31"
curl -H "X-Api-Key: $KEY" "$BASE/businesses/$BID/documents/$DOC_ID"

5.4 Open items: a receipt names the invoices it pays

A tax invoice (305) or proforma (300) is a debt until something pays it. That something is a receipt (400) that names it:

curl -X POST "$BASE/businesses/$BID/documents" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{
  "docType": 400, "party": {"partyId": "'$PARTY'"},
  "payments": [{"method": "transfer", "amount": 118000}],
  "settles": [{"documentId": "'$INVOICE_ID'", "amount": 118000}]
}'

settles is a list of issued 305/300 documents of the same customer, each with an optional agorot amount; without one the receipt covers as much of that document's balance as it can, in order. It is checked when the draft is created and again at issue (the balance may have moved), the receipt prints "על חשבון: חשבונית מס 1234 …" (הוראה 5: the receipt states the account it credits), and the invoice's balance falls by that amount. A credit note based on an invoice reduces its balance too; a negative receipt that reverses a receipt un-pays what it paid.

What you read back:

Money received without knowing which invoice it is for: issue the receipt with no settles, and match later with POST /documents/:id/settlements {"documentId", "amount?"} on the receipt (removable with DELETE .../settlements/:sid; an allocation printed on the receipt is not). A dueDate on a 305/300 prints "לתשלום עד" and anchors its ageing.

There is no law that ties a receipt number to an invoice number, and BillOS does not force one: a receipt with no settles is a valid receipt. What the practice settled on, here and in every Israeli invoicing product, is that the receipt says what it is for and the ledger keeps the balance.

5.5 Preview a draft: POST /businesses/:bid/documents/:id/preview

Returns the draft rendered as a "טיוטה"-watermarked PDF (raw application/pdf bytes) so your user can see exactly how the document will look before the irreversible issue. Nothing is recorded and no number is consumed. Optional body {"branding": {...}} overrides the stored branding for this render. Drafts only: an issued document's PDFs are frozen; download those via prints (409 not_draft otherwise).

curl -X POST "$BASE/businesses/$BID/documents/$DOC_ID/preview" -H "X-Api-Key: $KEY" \
  -H "Content-Type: application/json" -d '{}' -o preview.pdf

5.6 Issue: POST /businesses/:bid/documents/:id/issue

The point of no return. Assigns the next gapless document number, freezes a full snapshot, renders the PDF and signs it digitally.

curl -X POST "$BASE/businesses/$BID/documents/$DOC_ID/issue" -H "X-Api-Key: $KEY"
# 201 → {"ok":true,"document":{"docNumber":1071,"status":"issued","totalIncVat":190000,"vatAmount":28983,...}}

Check first: POST /businesses/:bid/documents/:id/validate. A dry run of issue. It runs every check issuing runs (account standing, the dealer type, totals, payments against the total, the allocation rule, the exchange rate, what a receipt settles) and stops before the first write: no number is taken, nothing is signed, the draft is unchanged.

curl -X POST "$BASE/businesses/$BID/documents/$DOC_ID/validate" -H "X-Api-Key: $KEY"
# 200 → {"ok":true,"valid":true,"wouldIssue":{"docType":320,"currency":"ILS","totalExVat":161017,"vatAmount":28983,"totalIncVat":190000,"allocation":"not_required"},"warnings":[]}
# or the exact refusal issue would give: {"error":"payments 30000 != total 35400","reason":"payments_mismatch","hint":"…","param":"payments",…}

Use it whenever a mistake would be expensive: an issued document is corrected only by a credit note. It needs documents:read, since it changes nothing.

5.7 Correct: POST /businesses/:bid/documents/:id/credit-note

The only way to "cancel" an issued 305/320. Issues a linked חשבונית מס זיכוי (330); for a receipt (400) it issues a negative receipt. The credit prints "בגין חשבונית מס 1234 (01/09/2026)" and your reason, which is what הוראה 23א asks a credit note to state: the invoice it refers to, the reason, the amount.

# In full: mirrors the original, the source is marked cancelled
curl -X POST "$BASE/businesses/$BID/documents/$DOC_ID/credit-note" -H "X-Api-Key: $KEY" \
  -H "Content-Type: application/json" -d '{"reason":"הזמנה בוטלה"}'

# In part, by amount (VAT included, agorot): the source stays open on the remainder
curl -X POST "$BASE/businesses/$BID/documents/$DOC_ID/credit-note" -H "X-Api-Key: $KEY" \
  -H "Content-Type: application/json" -d '{"reason":"הנחה שסוכמה בדיעבד","amount":23600}'

# In part, by lines: the credited items, as on the original
curl -X POST "$BASE/businesses/$BID/documents/$DOC_ID/credit-note" -H "X-Api-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"פריט הוחזר","lines":[{"description":"מנורת שולחן","quantity":1,"unitPriceExVat":20000}]}'
# 201 → { document: the credit note, source: { cancelledById, creditedAmount, remaining } }

A partial credit requires a reason, and the credits against one document can never add up to more than its total (הוראה 23א(2)(ב)); the moment they do, the source is marked cancelled like a full credit would. A 320 (חשבונית מס-קבלה) that gave money back can issue the negative receipt for the refund in the same call ("refund": true, or {"method": "card"}), the pair a partial credit of an invoice-receipt needs. A receipt is corrected by amount (amount = the part refunded); if it had paid invoices, that much is un-paid.

Send an Idempotency-Key (your refund id). A credit note is as permanent as the document it corrects: with the key, a call sent twice answers the credit note already issued with "deduped": true instead of a second one, and a key that already produced a different document is 409 conflict.

A חשבון עסקה (300) has no credit note. It is not a tax document, so a wrong one is voided: POST /businesses/:bid/documents/:id/void with an optional {"reason": "..."}. The document, its PDFs and its place in the exports stay; it reads as cancelled and stops being owed. It is refused once a receipt was settled against it (cancel the receipt first) and on any other type.

What the law says, in short: an invoice that has left your hands is never edited (הוראה 23); a transaction cancelled in whole or in part, a changed amount or an error is corrected by a הודעת זיכוי that names the invoice, the reason and the amount (23א); and reducing your VAT liability by it has its own conditions (the customer acknowledged it, or it went by registered mail) that are yours to meet, not the software's.

5.8 Get the PDF: POST /businesses/:bid/documents/:id/prints

Returns raw application/pdf bytes. kind is one of:

# a "download PDF" button can always just send auto:
curl -X POST "$BASE/businesses/$BID/documents/$DOC_ID/prints" -H "X-Api-Key: $KEY" \
  -H "Content-Type: application/json" -d '{"kind":"auto"}' -o invoice.pdf

409 {"reason":"pdf_pending"} → the PDF isn't rendered yet (rare, right after issue); retry with backoff; the origin is never consumed by a pdf_pending response. PDFs are PAdES-signed with a per-business certificate chained to the BillOS CA. Fetch the CA certificate at GET https://api.billos.co.il/ca.pem to verify signatures.

5.9 Allocation numbers (מספר הקצאה, חשבוניות ישראל)

Under the חשבוניות ישראל reform, a tax invoice (305/320) needs an allocation number from the Tax Authority when all of these hold: the amount before VAT exceeds the statutory threshold (₪5,000 since June 2026), the invoice carries VAT, and the buyer is an identified business, which needs the number to deduct input VAT. Credit notes (330) don't require one.

For a business connected to the Tax Authority, BillOS requests the number itself, inside the issue call: it comes back on the document as allocationNumber, prints on it under "מספר הקצאה", and is announced by document.allocation_assigned. Connecting is the owner's one-time consent (below). For a business that is not connected, or when the Authority refuses or does not answer, the document issues with a warning: allocation_missing when nothing was asked, allocation_not_approved, allocation_pending_decision or allocation_decision_sent when the Authority answered with its decision code (460, 461, 462), allocation_failed when it could not be reached; if the business opts into strict mode (PATCH /businesses/:bid {"requireAllocation": true}), issue returns 422 allocation_required instead and nothing is numbered. A number obtained elsewhere can also be supplied as allocationNumber on the draft. Most consumer-facing flows (B2C, receipts) never hit the threshold.

5.9.1 Connecting a business to the Tax Authority: POST /businesses/:bid/ita/connect

The Tax Authority grants the right to request numbers to a business, not to a software house, so the business's owner consents himself, once, on the Authority's own page with his own credentials. Your app starts it and gets him back:

POST /v1/businesses/<bid>/ita/connect
{ "returnUrl": "https://app.example.com/settings/tax", "state": "user-1842" }

→ 201 { "ok": true, "url": "https://openapi.taxes.gov.il/…", "expiresAt": "…", "sandbox": false }
  1. Send the owner to url: open it in his browser, or pass "link": true for an address you send him by email or WhatsApp (good for 24 hours instead of 15 minutes).
  2. He signs in at רשות המסים and approves BillOS.
  3. His browser lands on your returnUrl with ita=connected (or denied, or error with a reason), business=<bid> and your state.
  4. Confirm with GET /businesses/:bid/ita or the ita.connected webhook before you tell him it worked: a query string is something anyone can type.

returnUrl is not registered ahead. It must be https (http to localhost is accepted in the sandbox), at most 512 characters, with no credentials, no fragment, and none of the four parameters BillOS adds. state is yours: printable ASCII, up to 128 characters, echoed back unchanged. Only the business's owner (the key that created it) may start or remove a connection; anyone the business is open to may read it. A business that is pending, suspended or paused cannot be connected.

What BillOS holds is the authorization, encrypted, and it never leaves BillOS: you read a status, never a token.

{ "ok": true, "ita": {
  "status": "active", "connected": true,
  "connectedAt": "2026-10-27T09:12:00.000Z", "expiresAt": "2027-01-25T09:12:00.000Z", "daysLeft": 90,
  "configured": true, "sandbox": false } }

status is not_connected, active, expiring or expired. A consent lasts 90 days, and refreshing does not extend it: only a new consent does. From 7 days before the end the status reads expiring (still connected) and ita.expiring fires once; ask the owner to connect again then, through the same call, and the new consent replaces the old one. At the end it reads expired, ita.expired fires, and documents that need a number issue without one until he does. DELETE /businesses/:bid/ita removes the authorization (ita.disconnected).

reason on an error return: exchange_failed (the Authority refused the code), authority_unreachable (it did not answer in time after the owner approved: start again), authority_error, or wrong_mode. On denied it is owner_declined. A url past its expiresAt opens a BillOS page that says the link is no longer valid and does not return to you.

In the sandbox (a bk_test_ key) the same call returns a BillOS page that stands in for the Authority, with אישור and סירוב buttons; nothing reaches רשות המסים and no ITA credentials are involved. POST /businesses/:bid/ita/simulate {"state":"expiring"} (or "expired") moves a connected sandbox business to the end of its consent, so ita.expiring, ita.expired and your reminders can be tried today. A live key is refused (403 forbidden).

5.10 Recurring documents: POST /businesses/:bid/recurring

A plan is a template, not a document: the same fields POST /documents takes, plus a schedule. Every run (once a day, early morning Israel time) creates an ordinary draft with that day's date, or issues it when autoIssue: true, filed under externalRef: recurring:<planId>:<runDate> so a run can never double up, and writes a recurring.ran event (outcome: draft | issued | failed) that is the trigger for "email it" or "alert me" automations.

curl -X POST "$BASE/businesses/$BID/recurring" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{
  "docType": 305, "partyId": "'$PARTY'", "interval": "monthly", "dayOfMonth": 1,
  "lines": [ { "description": "מנוי חודשי", "quantity": 1, "unitPriceExVat": 50000 } ],
  "payments": [ { "method": "transfer", "amount": 59000, "dueDate": "2026-10-15" } ],
  "note": "לתשלום תוך 14 יום",
  "autoIssue": false
}'
# 201 → {"ok":true,"plan":{"id":"…","nextRunOn":"2026-10-01","active":true,…}}

5.11 Moving to BillOS: import the history

A business that switches to BillOS brings two things from its previous software: a numbering to continue, and seven years of documents it must still be able to produce. The first is startNumbers on POST /businesses (section 4): {"305": 1071} makes the first tax invoice here #1071, so the series the previous system left off is continued rather than restarted. The second is the archive:

curl -X POST "$BASE/businesses/$BID/legacy-documents" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"source":"morning","docType":320,"docNumber":"10042","issuedOn":"2025-03-14","partyName":"דנה ישראלי","totalIncVat":35400,"vatAmount":5400}'
# 201 → {"ok":true,"document":{"id":"…","source":"morning","docNumber":"10042",…,"file":null},"deduped":false}
curl -X PUT "$BASE/businesses/$BID/legacy-documents/$ID/file" -H "X-Api-Key: $KEY" -H "Content-Type: application/pdf" --data-binary @10042.pdf
curl "$BASE/businesses/$BID/legacy-documents?from=2025-01-01&to=2025-12-31&q=דנה" -H "X-Api-Key: $KEY"
curl "$BASE/businesses/$BID/legacy-documents/$ID/file" -H "X-Api-Key: $KEY" -o 10042.pdf

What an archived document is, and is not:

Sources people arrive from, as slugs: morning, icount, greeninvoice, ezcount, rivhit, sumit, hashavshevet, priority; any other a-z0-9_- slug is accepted.

5.12 Quotes (הצעת מחיר): POST /businesses/:bid/quotes

A quote is the offer BEFORE the sale, and it is deliberately not a document. It has no statutory number, it is freely editable and deletable, and it appears in no מבנה אחיד, no PCN874, no income report and no receivables figure. Israeli document law does not know it: נספח 1 has no code for it (100 is הזמנה, the customer's *order*, a different act), so giving it a docType would put a freely-edited row inside the permanent file and force every reader that sums money to remember to exclude it.

What it shares with a document is the machinery worth sharing: the same customer master, the same totals arithmetic (so the invoice repeats the offer to the agora), the same foreign-currency rules and the same two PDF designs. What it never wears is a tax document's furniture: no מקור/העתק, no "מסמך ממוחשב", no signature stamp, no allocation line. Each of those is a legal claim, and on an offer each would be a false one.

curl -X POST "$BASE/businesses/$BID/quotes" -H "X-Api-Key: $KEY" -H "Content-Type: application/json" -d '{
  "title": "טיפול שנתי במשרדים",
  "party": { "name": "מלון הצוק", "phone": "0501112223", "email": "office@example.com" },
  "priceMode": "gross",
  "validUntil": "2026-10-15",
  "terms": "המחיר כולל שני ביקורי מעקב. תשלום שוטף + 30.",
  "lines": [ { "description": "הדברה יסודית", "quantity": 1, "unitPriceExVat": 118000 } ]
}'
# 201 → {"ok":true,"quote":{"id":"…","state":"draft","number":null,"totalIncVat":118000,…}}

curl -X POST "$BASE/businesses/$BID/quotes/$QID/send" -H "X-Api-Key: $KEY"
# 201 → {"ok":true,"quote":{"number":1001,"state":"sent","publicUrl":"https://api.billos.co.il/q/…"},"pdf":{…}}