BillOS v1
Console llms.txt OpenAPI

Sandbox: test keys

Every key is either live (bk_live_…) or sandbox (bk_test_…). The API is the same in both: same base URL, same endpoints, same request and response shapes, same error codes. What differs is where the data goes.

A sandbox key answers from a separate BillOS database. Businesses, parties, documents, numbers, expenses and exports you create with it exist only there: nothing done with a test key can reach a real ledger, and nothing in a real ledger is visible to a test key. Every response says which side it came from:

X-BillOS-Mode: sandbox

Check that header on your first request. A live key pasted into a test environment issues real, numbered, immutable documents, and a test key pasted into production returns 404 for every business you created in live.

Getting a sandbox key: POST /sandboxes

The one resource in this API that takes no credentials: it is how a script, a CLI or a coding agent that holds no key yet asks for one. A person approves the request once, in the console.

curl -X POST "https://api.billos.co.il/v1/sandboxes" -H "Content-Type: application/json" -d '{"name":"my app"}'
# 202 → {"ok":true,"status":"authorization_required",
#        "verifyUrl":"https://dash.billos.co.il/device?code=KQ7M-2XHD","userCode":"KQ7M-2XHD",
#        "deviceCode":"…","expiresIn":600,"interval":3,"tokenUrl":"https://api.billos.co.il/v1/sandboxes/token"}

# the person opens verifyUrl, signs in and approves; meanwhile, every `interval` seconds:
curl -X POST "https://api.billos.co.il/v1/sandboxes/token" -H "Content-Type: application/json" -d '{"deviceCode":"…"}'
# 200 → {"ok":true,"status":"pending","interval":3}
# 200 → {"ok":true,"status":"approved","mode":"sandbox","apiKey":"bk_test_…","keyPrefix":"bk_test_3QfT", …}   (once)
# 403 → {"reason":"authorization_denied", …}   the person declined (once)

From a terminal, npx billos sandbox does all of it: it prints the link, waits, and stores the key.

Anonymous sandboxes are a server option, off by default. Where the operator enabled them, POST /sandboxes answers 201 with status: "ready" and an apiKey at once, for a sandbox nobody owns yet. Such a sandbox is small and short (30 requests a minute and 500 a day, one key, no ocr, an expiresAt 7 days out), may not register webhooks or Connect apps or have documents read, and a person keeps it by opening its claimUrl, which replaces the key: the one it was born with stops, and the claimer is shown a new one. A client reads status and handles both answers.

What behaves differently

LiveSandbox
Numberinggapless, permanentgapless within the sandbox business; the database may be reset
PDFssigned (PAdES), marked [מקור] / [העתק]a SANDBOX watermark on every render, a footer note that the document is not an accounting document, and not signed: issue returns warnings: ["unsigned"]
Frozen snapshotthe legal recordthe same shape, plus software.sandbox: true
Israel Tax Authority (allocation numbers)requested automatically for a connected businessnever called. POST /ita/connect opens a simulated consent page instead of the Authority's, and POST /ita/simulate moves the connection to its end. An invoice over the threshold issues with the allocation_missing warning, or is refused with allocation_required when the business has requireAllocation: true, so both paths are testable
Files (PDFs, receipts, exports, backups)object storage + off-site archivestored in the sandbox database only, never archived
OCRa billed AI call, ocr scope, daily capthe same: a scan costs the same in either mode
Rate limit60 requests/min per keythe same

Everything else (validation, totals and VAT math, idempotency, credit notes, prints, מבנה אחיד, the continuity report, backups) runs the same code and returns the same answers. If it works against the sandbox it works against live.

Fixtures: POST /sandbox/seed

Something to look at before you have written a line. With a bk_test_ key, one call creates a demo business (three customers; four issued documents covering the shapes worth seeing: a paid invoice-receipt, an unpaid tax invoice with a due date, a dollar document in English with zero-rated VAT, a cash receipt; one open draft; one recorded expense and one expense draft), through the same code the API runs, so numbering, PDFs, webhooks and the event log all behave as in live. Idempotent: calling it again returns the same business with seeded: false. A live key is refused.

curl -X POST "$BASE/sandbox/seed" -H "X-Api-Key: $TEST_KEY"
# 201 → {"ok":true,"seeded":true,"business":{…},"parties":[…],"documents":[…],"expenses":[…]}

Retention

Sandbox data is kept, but it is a test database: it is not backed up, it is not archived, and we may reset it (with notice). Do not use it as storage, and do not expect a sandbox business to exist in live.

Going live

  1. Get a live key. A sandbox key is self-serve (POST /sandboxes and an approval, or the console's keys page); a live key is opened in the console after a short check of the business setup, because an issued document is permanent. Both are shown once.
  2. Replace the key. Nothing else changes: same base URL, same code.
  3. Create each real business again with POST /businesses. Ids from the sandbox do not exist in live.
  4. Confirm X-BillOS-Mode: live on the first response.