Developers

REST API reference

Integrate Bilify programmatically. Manage clients, documents, items, expenses and statistics over a clean REST API, and receive signed webhooks on the events that matter.

Download OpenAPI spec View all endpoints
Base URL https://bilify.ddev.site/api/v1

Overview

Every request operates inside the workspace pinned to the API key that authenticates it, acting as the user who created that key, with exactly the permissions that user has in the dashboard.

This page is generated from the OpenAPI document that is the source of truth. Download the raw spec to import it into Redoc, Swagger UI or Postman.

Authentication

Send a Stripe-style opaque key as a bearer token in the Authorization header. Keys are created and revoked in the dashboard under Settings > Integrations, and are shown exactly once at creation.

  • blf_live_binds the real production workspace.
  • blf_test_binds the separate sandbox workspace (test data only).
Example request
curl https://bilify.ddev.site/api/v1/clients \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

A missing, malformed, unknown, revoked or expired key returns a single generic 401 that never reveals which part was wrong.

Conventions

Stable references

Resources are addressed by their public ULID reference, never a numeric id. Field names are snake_case.

Money

Amounts appear twice: a decimal string in the major unit and an integer of minor units, always with a currency code. Compute against the cents; display the string.

Pagination

List endpoints accept page and per_page (capped at 100) and return a meta block with page, per_page, total and last_page.

Idempotency

Write endpoints accept an Idempotency-Key header. The first successful response is stored for 24 hours and replayed on an identical retry, so a create never runs twice.

Rate limiting

Requests are limited per key (120 per minute by default). Every response carries X-RateLimit headers; a 429 includes Retry-After.

Dates

Dates are ISO 8601: YYYY-MM-DD for calendar dates and RFC 3339 timestamps for instants.

Errors

Every failure uses one envelope with a stable machine code, a human message, and, for validation failures only, a map of field errors.

Error envelope
{
  "error": {
    "code": "validation_failed",
    "message": "The given data was invalid.",
    "errors": { "name": ["The name field is required."] }
  }
}
Code HTTP status
validation_failed 422
unauthenticated 401
invalid_api_key 401
forbidden 403
not_found 404
rate_limited 429
plan_limit_reached 402
immutable_document 409
document_not_payable 409
idempotency_conflict 409
bad_request 400
method_not_allowed 405
conflict 409
server_error 500
no_workspace_bound 403
workspace_membership_revoked 403
no_active_plan 403
plan_upgrade_required 403
sandbox_not_provisioned 403
environment_mismatch 403
toolset_disabled 403

Clients

The workspace's customers.

Returns a paginated list of client summaries ordered by name. Requires the `clients.view` permission.

Requires permission clients.view

Parameters

type string query

Filter by client type.

business, individual

search string query

Free-text search filter.

page integer query

The 1-based page number.

per_page integer query

Items per page, capped at 100.

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/clients" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 A page of clients. ClientSummary[]
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Creates a client. Requires the `clients.create` permission.

Requires permission clients.create

Parameters

Idempotency-Key string header

Opt-in idempotency token. The first successful response is stored for 24h and replayed on an identical retry; reuse with a different body returns `idempotency_conflict`.

Request body

ClientCreate (required)

Example request

cURL
curl -X POST "https://bilify.ddev.site/api/v1/clients" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "string"
}'

Responses

  • 201 The created client. Client
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 409 The Idempotency-Key was reused with a different request body.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Returns one client by reference. Requires the `clients.view` permission.

Requires permission clients.view

Parameters

ulid * string path

The resource's public ULID reference.

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/clients/{ulid}" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 The client. Client
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 429 The per-key rate limit was exceeded.

Partially updates a client; omitted fields are left untouched. Requires the `clients.update` permission.

Requires permission clients.update

Parameters

ulid * string path

The resource's public ULID reference.

Request body

ClientUpdate (required)

Example request

cURL
curl -X PATCH "https://bilify.ddev.site/api/v1/clients/{ulid}" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "business",
    "name": "string",
    "legal_name": "string",
    "tax_number": "string"
}'

Responses

  • 200 The updated client. Client
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Documents

Invoices and other document types. A document is created as a numberless draft, edited freely, then issued as a deliberate step that assigns its number exactly once and freezes it. Only drafts are editable or deletable.

Returns a paginated list of document summaries, newest issue date first. Requires the `documents.view` permission.

Requires permission documents.view

Parameters

status string query

Filter by document status.

draft, issued, sent, viewed, partially_paid, paid, overdue, cancelled

client string query

Filter by client reference (ULID).

from string (date) query

Inclusive lower date bound (YYYY-MM-DD).

to string (date) query

Inclusive upper date bound (YYYY-MM-DD).

search string query

Free-text search filter.

page integer query

The 1-based page number.

per_page integer query

Items per page, capped at 100.

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/documents" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 A page of documents. DocumentSummary[]
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Creates a numberless draft document. Requires the `documents.create` permission. The document must be issued separately to receive its number.

Requires permission documents.create

Parameters

Idempotency-Key string header

Opt-in idempotency token. The first successful response is stored for 24h and replayed on an identical retry; reuse with a different body returns `idempotency_conflict`.

Request body

DocumentCreate (required)

Example request

cURL
curl -X POST "https://bilify.ddev.site/api/v1/documents" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "document_type": "string",
    "client": "string",
    "lines": []
}'

Responses

  • 201 The created draft document. Document
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 409 The Idempotency-Key was reused with a different request body.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Returns one document with its header, totals, tax breakdown, lines, and payments. Requires the `documents.view` permission.

Requires permission documents.view

Parameters

ulid * string path

The resource's public ULID reference.

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/documents/{ulid}" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 The document. Document
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 429 The per-key rate limit was exceeded.

Partially updates a draft document. Requires the `documents.update` permission. An already-issued document is immutable and returns `immutable_document` (409).

Requires permission documents.update

Parameters

ulid * string path

The resource's public ULID reference.

Request body

DocumentUpdate (required)

Example request

cURL
curl -X PATCH "https://bilify.ddev.site/api/v1/documents/{ulid}" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "document_type": "string",
    "client": "string",
    "currency": "string",
    "issue_date": "2026-10-03"
}'

Responses

  • 200 The updated draft document. Document
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 409 The document is issued and can no longer be edited or deleted.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Deletes a draft document. Requires the `documents.delete` permission. An issued document keeps its number for audit continuity and is cancelled, never deleted, so it returns `immutable_document` (409).

Requires permission documents.delete

Parameters

ulid * string path

The resource's public ULID reference.

Example request

cURL
curl -X DELETE "https://bilify.ddev.site/api/v1/documents/{ulid}" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 204 The draft document was deleted.
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 409 The document is issued and can no longer be edited or deleted.
  • 429 The per-key rate limit was exceeded.

Issues a draft document: assigns its number through the numbering service exactly once and freezes it. Requires the `documents.issue` permission. Issuing beyond the plan's monthly document limit is refused with `plan_limit_reached` (402). Re-issuing an already-issued document is a harmless no-op.

Requires permission documents.issue

Parameters

ulid * string path

The resource's public ULID reference.

Idempotency-Key string header

Opt-in idempotency token. The first successful response is stored for 24h and replayed on an identical retry; reuse with a different body returns `idempotency_conflict`.

Request body

DocumentIssue

Example request

cURL
curl -X POST "https://bilify.ddev.site/api/v1/documents/{ulid}/issue" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "issue_date": "2026-10-03"
}'

Responses

  • 200 The issued document. Document
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 402 Issuing would exceed the plan's monthly document limit.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 409 The Idempotency-Key was reused with a different request body.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Records a payment against an issued document and returns the refreshed document. Requires the `documents.update` permission. A draft or cancelled document cannot take a payment and returns `document_not_payable` (409).

Requires permission documents.update

Parameters

ulid * string path

The resource's public ULID reference.

Idempotency-Key string header

Opt-in idempotency token. The first successful response is stored for 24h and replayed on an identical retry; reuse with a different body returns `idempotency_conflict`.

Request body

DocumentPaymentCreate (required)

Example request

cURL
curl -X POST "https://bilify.ddev.site/api/v1/documents/{ulid}/payments" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 100,
    "method": "bank",
    "paid_on": "2026-10-03"
}'

Responses

  • 201 The document after the payment was applied. Document
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 409 A payment was attempted against a draft or cancelled document.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Items

The catalog of products and services. Stock is read-only in v1.

Returns a paginated list of item summaries ordered by name. Requires the `items.view` permission.

Requires permission items.view

Parameters

type string query

Filter by item type.

product, service

active boolean query

Filter by active state.

search string query

Free-text search filter.

page integer query

The 1-based page number.

per_page integer query

Items per page, capped at 100.

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/items" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 A page of items. ItemSummary[]
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Creates a product or service. Requires the `items.create` permission. `initial_stock` and `initial_stock_note` apply to products only.

Requires permission items.create

Parameters

Idempotency-Key string header

Opt-in idempotency token. The first successful response is stored for 24h and replayed on an identical retry; reuse with a different body returns `idempotency_conflict`.

Request body

ItemCreate (required)

Example request

cURL
curl -X POST "https://bilify.ddev.site/api/v1/items" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "product",
    "name": "string",
    "default_price": 100
}'

Responses

  • 201 The created item. Item
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 409 The Idempotency-Key was reused with a different request body.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Returns one item by reference. Requires the `items.view` permission.

Requires permission items.view

Parameters

ulid * string path

The resource's public ULID reference.

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/items/{ulid}" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 The item. Item
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 429 The per-key rate limit was exceeded.

Partially updates an item; omitted fields are left untouched. Requires the `items.update` permission. The item type and stock are fixed at creation and cannot be changed here.

Requires permission items.update

Parameters

ulid * string path

The resource's public ULID reference.

Request body

ItemUpdate (required)

Example request

cURL
curl -X PATCH "https://bilify.ddev.site/api/v1/items/{ulid}" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "string",
    "description": "string",
    "sku": "string",
    "unit": "string"
}'

Responses

  • 200 The updated item. Item
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Expenses

Recorded business expenses.

Returns a paginated list of expense summaries, newest occurrence first. Requires the `expenses.view` permission.

Requires permission expenses.view

Parameters

from string (date) query

Inclusive lower date bound (YYYY-MM-DD).

to string (date) query

Inclusive upper date bound (YYYY-MM-DD).

search string query

Free-text search filter.

page integer query

The 1-based page number.

per_page integer query

Items per page, capped at 100.

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/expenses" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 A page of expenses. ExpenseSummary[]
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Creates an expense. Requires the `expenses.create` permission.

Requires permission expenses.create

Parameters

Idempotency-Key string header

Opt-in idempotency token. The first successful response is stored for 24h and replayed on an identical retry; reuse with a different body returns `idempotency_conflict`.

Request body

ExpenseCreate (required)

Example request

cURL
curl -X POST "https://bilify.ddev.site/api/v1/expenses" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "string",
    "amount": 100,
    "occurs_on": "2026-10-03"
}'

Responses

  • 201 The created expense. Expense
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 409 The Idempotency-Key was reused with a different request body.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Returns one expense by reference. Requires the `expenses.view` permission.

Requires permission expenses.view

Parameters

ulid * string path

The resource's public ULID reference.

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/expenses/{ulid}" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 The expense. Expense
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 429 The per-key rate limit was exceeded.

Updates an expense. Requires the `expenses.update` permission. An expense generated from a recurring template keeps its scheduled date and records the change as an override.

Requires permission expenses.update

Parameters

ulid * string path

The resource's public ULID reference.

Request body

ExpenseUpdate (required)

Example request

cURL
curl -X PATCH "https://bilify.ddev.site/api/v1/expenses/{ulid}" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "string",
    "notes": "string",
    "amount": 100,
    "currency": "string"
}'

Responses

  • 200 The updated expense. Expense
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Deletes an expense. Requires the `expenses.delete` permission.

Requires permission expenses.delete

Parameters

ulid * string path

The resource's public ULID reference.

Example request

cURL
curl -X DELETE "https://bilify.ddev.site/api/v1/expenses/{ulid}" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 204 The expense was deleted.
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 429 The per-key rate limit was exceeded.

Statistics

Read-only aggregates powered by the same service as the dashboard: revenue/expense summary, outstanding receivables, top clients, and monthly trends.

Revenue, expenses, and net for a date range on a cash basis. Requires the `reports.view` permission.

Requires permission reports.view

Parameters

from string (date) query

Inclusive lower date bound (YYYY-MM-DD).

to string (date) query

Inclusive upper date bound (YYYY-MM-DD).

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/statistics/summary" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 The summary. BusinessStatistics
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

The receivables position with its aging breakdown at a point in time. Requires the `reports.view` permission.

Requires permission reports.view

Parameters

as_of string (date) query

The point in time to age against (defaults to now).

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/statistics/outstanding" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 The outstanding position. OutstandingStatistics
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Clients ranked by revenue received over a range. Requires the `reports.view` permission.

Requires permission reports.view

Parameters

limit integer query

Maximum number of clients to return.

from string (date) query

Inclusive lower date bound (YYYY-MM-DD).

to string (date) query

Inclusive upper date bound (YYYY-MM-DD).

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/statistics/top-clients" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 The ranked clients. TopClient[]
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Revenue, expenses, and net per month for the trailing window. Requires the `reports.view` permission.

Requires permission reports.view

Parameters

months integer query

Number of trailing months to include.

Example request

cURL
curl -X GET "https://bilify.ddev.site/api/v1/statistics/trends" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 200 The monthly trend points. MonthlyTrend[]
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 422 The request failed validation. `errors` maps each field to its messages.
  • 429 The per-key rate limit was exceeded.

Webhooks

Management of outbound webhook deliveries.

Resets a webhook delivery to pending and re-queues it for another attempt. Requires the `integrations.manage` permission.

Requires permission integrations.manage

Parameters

ulid * string path

The resource's public ULID reference.

Idempotency-Key string header

Opt-in idempotency token. The first successful response is stored for 24h and replayed on an identical retry; reuse with a different body returns `idempotency_conflict`.

Example request

cURL
curl -X POST "https://bilify.ddev.site/api/v1/webhook-deliveries/{ulid}/redeliver" \
  -H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Responses

  • 202 The delivery was queued for redelivery. object
  • 401 Missing, malformed, unknown, revoked, or expired API key.
  • 403 The acting user lacks the permission this endpoint requires.
  • 404 No such resource in the bound workspace.
  • 409 The Idempotency-Key was reused with a different request body.
  • 429 The per-key rate limit was exceeded.

Webhooks

Bilify delivers outbound, HMAC-signed webhooks on domain events to endpoints you register per environment under Settings > Integrations. A delivery is retried on a backoff schedule, then marked dead; an endpoint whose recent deliveries all fail is auto-disabled.

Event catalog

client.created client.updated document.created document.issued document.updated document.deleted payment.recorded expense.created expense.updated expense.deleted

Payload

Each delivery carries the event name, when it occurred, the environment, and the subject under data, in the same shape the matching GET endpoint returns.

Example payload
{
  "event": "document.issued",
  "occurred_at": "2026-07-14T10:32:00+00:00",
  "environment": "production",
  "data": { "reference": "01J...", "number": "0001", "status": "issued" }
}

Signature

Each delivery is signed with the X-Bilify-Signature header, in the Stripe-compatible scheme t=<unix>,v1=<hmac_sha256(secret, t + "." + body)>. Recompute the HMAC over the timestamp and the raw body, compare in constant time, and reject timestamps outside the 5 minute tolerance window.

X-Bilify-Signature
X-Bilify-Signature: t=1720952400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Verify a signature (PHP)

PHP
<?php

function bilify_webhook_is_valid(string $secret, string $header, string $rawBody, int $tolerance = 300): bool
{
    $parts = [];
    foreach (explode(',', $header) as $piece) {
        [$k, $v] = array_pad(explode('=', trim($piece), 2), 2, '');
        $parts[$k] = $v;
    }

    if (! isset($parts['t'], $parts['v1']) || ! ctype_digit($parts['t'])) {
        return false;
    }

    if (abs(time() - (int) $parts['t']) > $tolerance) {
        return false;
    }

    $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);

    return hash_equals($expected, $parts['v1']);
}

$rawBody = file_get_contents('php://input');
$header  = $_SERVER['HTTP_X_BILIFY_SIGNATURE'] ?? '';

if (! bilify_webhook_is_valid($webhookSecret, $header, $rawBody)) {
    http_response_code(400);
    exit;
}

Verify a signature (Node.js)

Node.js
const crypto = require('crypto');

function bilifyWebhookIsValid(secret, header, rawBody, tolerance = 300) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.trim().split('='))
  );

  if (!parts.t || !parts.v1) return false;

  const timestamp = parseInt(parts.t, 10);
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > tolerance) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Schemas

The response and request shapes referenced above. Fields marked with an asterisk are required; a trailing question mark on a type marks it nullable.

ErrorEnvelope

error * object

PaginationMeta

page integer
per_page integer
total integer
last_page integer

ClientSummary

reference string

Public ULID.

name string
type string?

business | individual

tax_number string?
email string?
city string?
default_currency string?

Client

reference string

Public ULID.

name string
legal_name string?
type string?

business | individual

tax_number string?
registration_number string?
email string?
phone string?
address_line1 string?
address_line2 string?
city string?
postal_code string?
country string?

ISO 3166-1 alpha-2 code.

default_currency string?
portal_access_enabled boolean
notes string?
created_at string (date-time)?

ClientCreate

type string

Defaults to business.

business | individual

name * string
legal_name string?
tax_number string?

Unique within the workspace.

registration_number string?
email string (email)?

Unique within the workspace.

phone string?
address_line1 string?
address_line2 string?
city string?
postal_code string?
country string?

ISO 3166-1 alpha-2 code.

default_currency string?
portal_access_enabled boolean
notes string?

ClientUpdate

All fields optional; omitted fields are left untouched.

type string

business | individual

name string
legal_name string?
tax_number string?
registration_number string?
email string (email)?
phone string?
address_line1 string?
address_line2 string?
city string?
postal_code string?
country string?
default_currency string?
portal_access_enabled boolean
notes string?

ClientRef

reference string
name string

DocumentSummary

reference string

Public ULID.

number string?

Null until issued.

status string?

draft | issued | sent | viewed | partially_paid | paid | overdue | cancelled

type string?

Document type key.

type_name string?
client ClientRef
issue_date string (date)?
due_date string (date)?
currency string
total Money
total_cents integer
paid Money
paid_cents integer
balance Money
balance_cents integer

Document

reference string

Public ULID.

number string?
status string?

draft | issued | sent | viewed | partially_paid | paid | overdue | cancelled

type string?

Document type key.

type_name string?
client ClientRef
currency string
issue_date string (date)?
due_date string (date)?
delivery_date string (date)?
issued_at string (date-time)?
sent_at string (date-time)?
subtotal Money
subtotal_cents integer
tax_total Money
tax_total_cents integer
total Money
total_cents integer
paid Money
paid_cents integer
balance Money
balance_cents integer
tax_breakdown array<object>

Frozen tax bands at issue.

lines array<DocumentLine>
payments array<DocumentPayment>

DocumentLine

position integer
description string?
item_reference string?

Catalog item ULID, if linked.

quantity string
unit_price Money
unit_price_cents integer
tax_rate string?

Snapshot tax percentage.

line_subtotal Money
line_tax Money
line_total Money
line_total_cents integer

DocumentPayment

amount Money
amount_cents integer
currency string
method string?

bank | cash | card | gateway

paid_at string (date-time)?

DocumentLineInput

description string?
unit string?
quantity * number
unit_price * number

Major-unit price.

item string?

Catalog item ULID to link.

tax_rate string?

Tax rate key for the workspace country.

discount_type string?

percent | fixed

discount_value number?

DocumentCreate

document_type * string

Active document type key for the workspace country.

client * string

Client ULID.

currency string?

Defaults to the client's, then the workspace's currency.

issue_date string (date)?
due_date string (date)?
delivery_date string (date)?
lines * array<DocumentLineInput>

DocumentUpdate

Drafts only; all fields optional. Sending `lines` replaces the whole set.

document_type string
client string

Client ULID.

currency string?
issue_date string (date)?
due_date string (date)?
delivery_date string (date)?
lines array<DocumentLineInput>

DocumentIssue

issue_date string (date)?

Overrides the issue date at issue time.

DocumentPaymentCreate

amount * number

Major-unit amount.

method * string

bank | cash | card | gateway

paid_on * string (date)

ItemSummary

reference string

Public ULID.

name string
sku string?
type string?

service | product

unit string
category string?
default_price Money
default_price_cents integer
currency string
active boolean
stock_quantity string?

Products only.

is_low_stock boolean

Item

reference string

Public ULID.

name string
description string?
sku string?
type string?

service | product

unit string
category string?
default_price Money
default_price_cents integer
currency string
default_tax_rate string?

Percentage, as a string.

active boolean
stock_quantity string?
low_stock_threshold string?
is_low_stock boolean
created_at string (date-time)?

ItemCreate

type * string

Fixed at creation.

product | service

name * string
description string?
sku string?

Unique within the workspace.

unit string

Scoped to the item type. Products accept pcs, kg, l, m, day, hour, service and default to pcs; services accept only hour, day, service and default to hour.

default_price * number

Major-unit price.

currency string?
tax_rate string?

Tax rate key for the workspace country.

low_stock_threshold number?

Products only.

active boolean

Defaults to true.

initial_stock number?

Products only; opening stock quantity.

initial_stock_note string?

ItemUpdate

All fields optional. Type and stock cannot be changed here.

name string
description string?
sku string?
unit string

Scoped to the item type. Products accept pcs, kg, l, m, day, hour, service; services accept only hour, day, service.

default_price number
currency string?
tax_rate string?
low_stock_threshold number?
active boolean

ExpenseSummary

reference string

Public ULID.

description string
vendor string?
category string?
occurs_on string (date)?
amount Money
amount_cents integer
currency string
is_recurring boolean

Expense

reference string

Public ULID.

description string
notes string?
vendor string?
category string?
occurs_on string (date)?
amount Money
amount_cents integer
currency string
is_recurring boolean
is_overridden boolean
created_at string (date-time)?

ExpenseCreate

description * string
notes string?
amount * number

Major-unit amount.

currency string?
category string?

Category name. Created automatically if it does not already exist in this workspace.

vendor string?
occurs_on * string (date)

ExpenseUpdate

All fields optional.

description string
notes string?
amount number
currency string?
category string?

Category name. Created automatically if it does not already exist. An empty string clears it.

vendor string?
occurs_on string (date)

BusinessStatistics

period_start string

ISO date.

period_end string

ISO date.

currency string
revenue Money
revenue_cents integer
expenses Money
expenses_cents integer
net Money
net_cents integer

OutstandingStatistics

currency string
outstanding Money
outstanding_cents integer
overdue Money
overdue_cents integer
document_count integer
overdue_count integer
aging_buckets array<AgingBucket>

AgingBucket

key string
min_days integer
max_days integer?

Null means open-ended (e.g. 90+).

amount Money
amount_cents integer
document_count integer

TopClient

client_reference string

Client ULID.

client_name string
currency string
revenue Money
revenue_cents integer
document_count integer

MonthlyTrend

month string

YYYY-MM key.

period_start string

ISO date.

period_end string

ISO date.

currency string
revenue Money
revenue_cents integer
expenses Money
expenses_cents integer
net Money
net_cents integer

WebhookEnvelope

The body POSTed to a subscribed webhook endpoint. `data` is the same resource representation the matching GET endpoint returns.

event string

client.created | client.updated | document.created | document.issued | document.updated | document.deleted | payment.recorded | expense.created | expense.updated | expense.deleted

occurred_at string (date-time)
environment string

production | sandbox

data object

The subject resource (a Client, Document, or Expense representation).