Biflus API
Read and write your Biflus data from your own code: clients, items, categories, and invoices. Whether you're connecting Zapier, syncing another tool, or just automating something for yourself, this is everything you need.
https://api.biflus.com
·
Last updated 2026-07-30
Quick start
The whole API follows one pattern, so seeing it once tells you almost everything. Once you have a key (see Authentication), here's listing your clients:
GET https://api.biflus.com/v1/clients Authorization: Bearer sk_test_9f2c7a1e4b8d3f0a6c5e2b1d8f4a7c3e
Response
{ "data": [ { "_id": "1785406744124x246658754867582050", "company_name": "Acme Corp", "type": "Company" /* … */ } ], "pagination": { "count": 1, "remaining": 0, "next_cursor": null } }
Every other resource (items, categories, invoices) works the same way:
GET to list, GET .../:id
to fetch one, POST to create,
PATCH to update,
DELETE to remove. If you know one, you know them all.
Authentication
Every request is authenticated with an API key, sent as a bearer token:
Authorization: Bearer sk_test_9f2c7a1e4b8d3f0a6c5e2b1d8f4a7c3e
Keys are prefixed sk_test_ or
sk_live_; the prefix alone determines which
Bubble environment (test or live) a request reads and writes to, so a key
only ever works against the data it was minted for.
A key can be revoked at any time; revoked keys are rejected immediately on the next request, with no grace period.
Making requests
Base URL
https://api.biflus.com. Every path in this
doc (/v1/clients, etc.) hangs off that.
Rate limits
100 requests per minute, per API key. Exceeding it returns
429; see Errors.
Pagination
List endpoints (GET /v1/clients,
/v1/items, etc.) accept
?limit (default 25, max 100) and
?cursor. Every list response includes a
pagination object: pass its
next_cursor back in as ?cursor
to get the next page; a null cursor means you're
at the end.
{ "data": [ /* … */ ], "pagination": { "count": 28, "remaining": 3, "next_cursor": "MjU=" } }
Errors
Errors are JSON with an error message, and
sometimes a detail or
valid_options field with more context.
{ "error": "\"cyan\" is not a valid icon", "valid_options": ["boxes", "rocket", …] }
| 400 | Missing or invalid fields in the request body. |
| 401 | Missing, invalid, or revoked API key. |
| 404 | The record doesn't exist, or belongs to a different account. |
| 409 | Blocked: another record still depends on this one (e.g. deleting a category still used by an item). |
| 422 | Blocked by content moderation. |
| 429 | Rate limit exceeded: 100 requests/minute per key. |
| 502 | An upstream service (Bubble, or PDF generation) failed. |
Clients
The people or companies you invoice.
| Field | Type | Notes |
|---|---|---|
| company_name | string required | The client's own name (doubles as a person's name when type is Individual). |
| type | "Individual" | "Company" required | |
| string | ||
| phone | string | |
| contact_phone | string | Only meaningful when type is Company. |
| color | string | Auto-assigned for Company clients if omitted. |
| address, city, postal_code, country | string | |
| tax_id | string |
Example
Request
{ "company_name": "Acme Corp", "type": "Company", "email": "billing@acme.com", "country": "UK" }
Response · 201
{ "data": { "_id": "1785406744124x246658754867582050", "company_name": "Acme Corp", "type": "Company", "email": "billing@acme.com", "country": "UK", "color": "Indigo" } }
Items
Your catalog: the things you sell, used as invoice line items.
| Field | Type | Notes |
|---|---|---|
| name | string required | |
| default_price | number required* | *Unless no_price_yet: true is sent instead. |
| description, sku | string | |
| unit | enum | Hour, Day, Piece, Session, Project, Kilogram, Liter, … (21 total) |
| icon | enum | Defaults to "boxes". 54 valid values (rocket, dog, gauge, …). |
| category | string (id) | A category id from Categories. |
| vat_rate | number (%) | e.g. 19. Resolved to (or creates) a matching tax rate. |
| active, track_stock_quantity | boolean | |
| stock_quantity | number | Setting this on create also sets the item's starting stock snapshot. |
Example
Request
{ "name": "Consulting Hour", "default_price": 150, "unit": "Hour", "vat_rate": 19 }
Response · 201
{ "data": { "_id": "1785406746780x104116111658747680", "name": "Consulting Hour", "default_price": 150, "unit": "Hour", "icon": "boxes", "default_vat_rate": "1785406801418x619076360946587600" } }
Categories
Groups for organizing items.
| Field | Type | Notes |
|---|---|---|
| name | string required | |
| color | string | Auto-assigned if omitted. |
| key_letter | string | The 2-letter chip shown in the app. Auto-derived from name if omitted. |
Invoices
Creating an invoice also generates and attaches a real PDF.
Line items
Each entry in items is either a catalog item
(item_id, price/VAT pulled from that item unless
overridden) or a custom one-off line (title +
unit_price directly, not saved to your catalog).
| Field | Type | Notes |
|---|---|---|
| client_id | string (id) required | |
| items | array required | At least one line. Each: item_id or title, plus optional quantity (default 1), unit_price, vat_rate. |
| issue_date, due_date | date string | Default to today, and today + your account's default payment terms. |
Example
Request
{ "client_id": "1785406744124x246658754867582050", "items": [ { "item_id": "1785406746780x104116111658747680", "quantity": 2 }, { "title": "One-off setup fee", "unit_price": 50 } ] }
Response · 201
{ "data": { "_id": "1785406752229x437683015977495360", "invoice_number": "IPP/1028", "status": "Draft", "subtotal": 350, "vat_amount": 0, "total": 350, "currency": "EUR", "pdf_file_URL": "https://invoices.biflus.com/pdfs/…/IPP1028.pdf" }, "line_items": [ /* full resolved line breakdown */ ] }
Example
Request
{ "items": [ { "item_id": "1785406746780x104116111658747680", "quantity": 3 } ] }
Response · 200
{ "data": { "total": 300, "pdf_file_URL": "https://invoices.biflus.com/pdfs/…/IPP1029.pdf" /* … */ }, "line_items": [ /* full resolved line breakdown, quantities recalculated */ ] }
Pass the full new list of line items, not just the ones changing. Old lines are replaced, not merged. The PDF is regenerated automatically.
Example: marking Paid
Request
{ "status": "Paid", "payment_method": "Bank Transfer", "payment_reference": "TX-12345" }
Response · 200
{ "data": { "status": "Paid", "payment_record": "1785429702412x796431784023203200" /* … */ }, "receipt_url": "https://invoices.biflus.com/pdfs/…/REC-IPP1030.pdf" }
Status only moves forward: once an invoice leaves Draft it can't go
back, and Paid/Canceled are final. Marking Paid also creates a payment
record and generates a receipt PDF automatically.
payment_method,
payment_reference, and
payment_notes are all optional.
Webhooks
Get notified the moment something happens, instead of polling for it.
Events
invoice.created,
invoice.sent,
invoice.paid,
invoice.canceled. Each delivery's body carries
the full invoice record under data.
{ "event": "invoice.paid", "created_at": "2026-07-30T17:45:26.525Z", "data": { "_id": "…", "invoice_number": "IPP/1031", "status": "Paid" /* … */ } }
Verifying a delivery
Every request carries an X-Biflus-Signature
header: sha256=<hex>, an HMAC-SHA256 of
the raw request body using your webhook's secret.
Recompute it on your end and compare before trusting the payload.
// Node.js example const crypto = require('crypto'); const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); const valid = expected === req.headers['x-biflus-signature'];
Delivery and retries
A 2xx response marks a delivery successful.
Anything else (including a timeout, 10s) is retried automatically with
backoff — 1 minute, 5 minutes, 30 minutes, then 2 hours — up to 5 attempts
total, after which it's marked dead and given up on.
| Field | Type | Notes |
|---|---|---|
| url | string required | Must be http(s). |
| events | array required | At least one, from the list above. |
| active | boolean | Defaults to true on create. Set false to pause without deleting. |
Example
Request
{ "url": "https://yourapp.com/webhooks/biflus", "events": ["invoice.paid", "invoice.sent"] }
Response · 201
{ "data": { "_id": "1785433508831x140304457758445360", "url": "https://yourapp.com/webhooks/biflus", "events": ["invoice.paid", "invoice.sent"], "active": true, "secret": "a3f8c1…" } }
Save secret — it's how you verify deliveries are really from Biflus.
Roadmap
What's built, and what's next. Kept current as we go.
Changelog
Webhooks shipped: subscribe to invoice events instead of polling.
invoice.created,invoice.sent,invoice.paid,invoice.canceled.- Every delivery is signed (HMAC-SHA256) so you can verify it's really from Biflus.
- Failed deliveries retry automatically with backoff, up to 5 attempts.
The API is now publicly reachable at https://api.biflus.com, and this documentation moved to its own site at docs.biflus.com.
Invoice lifecycle completed: edit and delete Drafts, and move a Draft through Sent, Paid, or Canceled.
- Marking an invoice Paid creates a real payment record and generates a receipt PDF automatically.
- Status only moves forward: once sent, an invoice can't go back to Draft; Paid and Canceled are final.
- Editing or deleting only works while an invoice is still a Draft.
Invoice creation shipped, with real PDF generation attached automatically.
Write endpoints shipped for clients, items, and categories.
- Create, update, and delete, each enforcing the same rules as the main app (required fields, valid icon/unit choices, content moderation on every write).
- Deleting a category or client that's still in use is blocked, not silently allowed.
Initial read-only API shipped: invoices, clients, items, categories.
- API key authentication, 100 req/min rate limiting, cursor pagination.