REST API quickstart
Make your first Bilify REST API calls, list clients and create a draft invoice, and learn the rules for auth, limits, retries and errors.
The REST API gives your own software the same records you work with in the app: clients, documents (invoices and the other document types), items, expenses, contracts and statistics. This quickstart takes you from a new key to a draft invoice in a few minutes. The full reference with every endpoint and field is on the public API reference page.
Before you start
- An API key. Create one in Settings > Integrations under REST API (see API keys). Use a sandbox key (
blf_test_) while you build, so nothing real is touched. - The Base URL, shown in the same section with a copy button. It ends in
/api/v1. There is one base URL for production and sandbox; the key decides which workspace you reach.
The examples below assume two shell variables:
export BILIFY_URL="https://<your Bilify address>/api/v1" # copy the Base URL from Settings > Integrations
export BILIFY_KEY="blf_test_..." # your key
Authentication
Send the key as a bearer token on every request:
Authorization: Bearer blf_test_...
The key acts as the person who created it, with that person's role permissions in the workspace. A call that needs a permission the person does not have returns 403 forbidden, exactly as the button would be missing in the app.
Step 1: list your clients
curl -s "$BILIFY_URL/clients?per_page=5" \
-H "Authorization: Bearer $BILIFY_KEY" \
-H "Accept: application/json"
The response wraps the rows in data and adds a meta block:
{
"data": [
{ "reference": "01J9Z...", "name": "Alfa Trade DOOEL", "type": "business",
"tax_number": "4030...", "email": "office@alfa.mk", "city": "Skopje",
"default_currency": "MKD" }
],
"meta": { "page": 1, "per_page": 5, "total": 42, "last_page": 9 }
}
Every record is addressed by its reference (a ULID), never by a numeric id. You can filter clients with search (name, tax number or email) and type.
Step 2: create a draft invoice
Documents are created as drafts: no number, freely editable, not visible to the client. Document types and tax rates are addressed by their key, for example mk.faktura (Фактура) and mk.vat.standard (VAT 18%) for a Macedonian workspace.
curl -s -X POST "$BILIFY_URL/documents" \
-H "Authorization: Bearer $BILIFY_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: order-10045" \
-d '{
"document_type": "mk.faktura",
"client": "01J9Z...",
"currency": "MKD",
"due_date": "2026-10-20",
"lines": [
{ "description": "Web design", "quantity": 1, "unit_price": 12000, "tax_rate": "mk.vat.standard" }
]
}'
A successful call returns 201 with the draft under data, including its reference. Prices are in the major unit (denars, euros). If you leave out currency, the client's default currency is used, then the workspace's. The currency must have an active payment method in Settings, otherwise you get a validation_failed error on currency.
Step 3: issue it (optional)
Issuing is a separate, deliberate call:
curl -s -X POST "$BILIFY_URL/documents/01JA0.../issue" \
-H "Authorization: Bearer $BILIFY_KEY" \
-H "Accept: application/json"
Issuing is final
Issuing assigns the official number, freezes the totals and the client details, and counts towards your package's monthly document limit. After that the document can no longer be changed or deleted with PATCH or DELETE (you get 409 immutable_document). Issue only when the invoice is really ready.
Endpoints at a glance
| Area | Endpoints |
|---|---|
| Clients | GET /clients, POST /clients, GET /clients/{reference}, PATCH /clients/{reference} |
| Documents | GET /documents, POST /documents, GET, PATCH, DELETE /documents/{reference} (drafts only), POST /documents/{reference}/issue, POST /documents/{reference}/payments |
| Items | GET /items, POST /items, GET /items/{reference}, PATCH /items/{reference} |
| Expenses | GET /expenses, POST /expenses, GET, PATCH, DELETE /expenses/{reference} |
| Contracts | GET /contracts, POST /contracts, GET /contracts/{reference}, POST /contracts/{reference}/send, POST /contracts/{reference}/void, GET /contracts/{reference}/signed.pdf |
| Statistics | GET /statistics/summary, /statistics/outstanding, /statistics/top-clients, /statistics/trends |
| Webhooks | POST /webhook-deliveries/{reference}/redeliver |
Contracts created through the API are always written contracts. Uploading a PDF contract is only possible in the app.
Conventions
- Money comes twice: a decimal string such as
"1250.00"and an integer in minor units such as125000in the matching_centsfield. Calculate with the_centsvalue. - Dates are
YYYY-MM-DD; timestamps are ISO 8601. - Pagination: list endpoints take
pageandper_page(default 25, maximum 100) and returnmeta.page,meta.per_page,meta.totalandmeta.last_page.
Rate limit
Each key may make 120 requests per minute. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining. Above the limit you get 429 rate_limited with a Retry-After header that says how many seconds to wait.
Safe retries with Idempotency-Key
The POST endpoints that create or change something (clients, documents, issue, payments, items, expenses, contracts, send, void, redeliver) accept an Idempotency-Key header. Use a value that is unique per operation, such as your own order id.
- The first successful (2xx) response for that key is stored for 24 hours. Retrying the same request with the same key returns the stored response with the header
Idempotency-Replayed: true, and nothing is created twice. - Reusing the key with a different body returns
409 idempotency_conflict. - Failed responses are not stored, so after fixing a validation error you can retry with the same key.
- Without the header, the request simply runs.
PATCH and DELETE do not use idempotency keys.
Errors
Every error has the same shape:
{
"error": {
"code": "validation_failed",
"message": "The request data was invalid.",
"errors": { "lines.0.unit_price": ["The lines.0.unit_price field is required."] }
}
}
code is stable and safe to branch on; message is human text and may change. errors appears only for validation failures.
| Status | Codes |
|---|---|
| 401 | invalid_api_key, unauthenticated |
| 402 | plan_limit_reached, feature_unavailable |
| 403 | forbidden, plan_upgrade_required, no_active_plan, workspace_membership_revoked, sandbox_not_provisioned, toolset_disabled (an area of the API is temporarily switched off by Bilify) |
| 404 | not_found |
| 409 | immutable_document, document_not_payable, idempotency_conflict, contract_not_sendable, contract_state_conflict |
| 422 | validation_failed |
| 429 | rate_limited |
Next steps
- Get notified instead of polling: Webhooks.
- Build against test data: Sandbox mode.
- Read every field and response: the API reference. The full endpoint index and the OpenAPI file are linked there and open once you are signed in.
Was this page helpful?
Related articles
Still stuck?
Write to us and we will get back to you within one working day.