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.

5 min read Updated 04.10.2026 Requires: REST API & webhooks

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 as 125000 in the matching _cents field. Calculate with the _cents value.
  • Dates are YYYY-MM-DD; timestamps are ISO 8601.
  • Pagination: list endpoints take page and per_page (default 25, maximum 100) and return meta.page, meta.per_page, meta.total and meta.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.