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.
Endpoints
Clients Documents Items Expenses Statistics WebhooksReference
Webhooks SchemasOverview
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).
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": {
"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 -X GET "https://bilify.ddev.site/api/v1/clients" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200A page of clients. ClientSummary[] -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X POST "https://bilify.ddev.site/api/v1/clients" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"name": "string"
}'
Responses
-
201The created client. Client -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
409The Idempotency-Key was reused with a different request body. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X GET "https://bilify.ddev.site/api/v1/clients/{ulid}" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200The client. Client -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
429The 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 -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
-
200The updated client. Client -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X GET "https://bilify.ddev.site/api/v1/documents" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200A page of documents. DocumentSummary[] -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -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
-
201The created draft document. Document -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
409The Idempotency-Key was reused with a different request body. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X GET "https://bilify.ddev.site/api/v1/documents/{ulid}" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200The document. Document -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
429The 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 -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
-
200The updated draft document. Document -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
409The document is issued and can no longer be edited or deleted. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X DELETE "https://bilify.ddev.site/api/v1/documents/{ulid}" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
204The draft document was deleted. -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
409The document is issued and can no longer be edited or deleted. -
429The 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
Example request
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
-
200The issued document. Document -
401Missing, malformed, unknown, revoked, or expired API key. -
402Issuing would exceed the plan's monthly document limit. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
409The Idempotency-Key was reused with a different request body. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -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
-
201The document after the payment was applied. Document -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
409A payment was attempted against a draft or cancelled document. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X GET "https://bilify.ddev.site/api/v1/items" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200A page of items. ItemSummary[] -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -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
-
201The created item. Item -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
409The Idempotency-Key was reused with a different request body. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X GET "https://bilify.ddev.site/api/v1/items/{ulid}" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200The item. Item -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
429The 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 -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
-
200The updated item. Item -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X GET "https://bilify.ddev.site/api/v1/expenses" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200A page of expenses. ExpenseSummary[] -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -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
-
201The created expense. Expense -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
409The Idempotency-Key was reused with a different request body. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X GET "https://bilify.ddev.site/api/v1/expenses/{ulid}" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200The expense. Expense -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
429The 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 -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
-
200The updated expense. Expense -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X DELETE "https://bilify.ddev.site/api/v1/expenses/{ulid}" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
204The expense was deleted. -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
429The 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 -X GET "https://bilify.ddev.site/api/v1/statistics/summary" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200The summary. BusinessStatistics -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X GET "https://bilify.ddev.site/api/v1/statistics/outstanding" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200The outstanding position. OutstandingStatistics -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X GET "https://bilify.ddev.site/api/v1/statistics/top-clients" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200The ranked clients. TopClient[] -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X GET "https://bilify.ddev.site/api/v1/statistics/trends" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
200The monthly trend points. MonthlyTrend[] -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
422The request failed validation. `errors` maps each field to its messages. -
429The 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 -X POST "https://bilify.ddev.site/api/v1/webhook-deliveries/{ulid}/redeliver" \
-H "Authorization: Bearer blf_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Responses
-
202The delivery was queued for redelivery. object -
401Missing, malformed, unknown, revoked, or expired API key. -
403The acting user lacks the permission this endpoint requires. -
404No such resource in the bound workspace. -
409The Idempotency-Key was reused with a different request body. -
429The 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.
{
"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: t=1720952400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Verify a signature (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)
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).