API v1

The BizBrada API

Push sales and expenses from your own till, POS or app into BizBrada, raise invoices, and read the books back. Same records, same tax position, no double entry.

Not a developer? See what you can connect in plain English — POS, online store, your own app.

Quick start
# 1. Confirm the API is up
curl https://bizbrada.com/api/public/v1/health

# 2. Read your books
curl "https://bizbrada.com/api/public/v1/transactions?limit=5" \
  -H "Authorization: Bearer bb_YOUR_KEY"

# 3. Record a sale
curl -X POST https://bizbrada.com/api/public/v1/transactions \
  -H "Authorization: Bearer bb_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "sale", "amount_minor": 250000, "occurred_on": "2026-01-14", "description": "Two cartons" }'

Overview

A JSON over HTTPS API. Every request is scoped to one business — the business the key belongs to — so there is no tenant parameter to pass and no way to read another business's records.

Base URL

https://bizbrada.com/api/public/v1

Plan access

Included on Scale. Available on Growth as a paid add-on. Not available on Free or Starter. See pricing.

Authentication

Create keys in Settings → API. A key is shown once, at creation. We store only a hash of it, so we cannot show it to you again — if it is lost, revoke it and make a new one.

Every request
curl https://bizbrada.com/api/public/v1/transactions \
  -H "Authorization: Bearer bb_YOUR_KEY"

read

GET endpoints.

write

POST, PATCH and DELETE endpoints.

webhooks

Manage webhook subscriptions.

  • A key with the wrong scope gets 403, not 401.
  • Rate limit is per key, per minute (60 by default). Over the limit returns 429 with a Retry-After header.
  • Keys can carry an expiry date and can be revoked at any time. Revoked keys return 401 immediately.
  • Every call is logged with a request id, method, path and status for your audit trail.
  • Signed-in owners can see request volume, per-key rate-limit headroom and the last 90 days of call history in Settings → API.

Conventions

  • Money is in minor units. 250000 means ₦2,500.00. Never send decimals for amounts.
  • Currency and rate travel together. Records can be in NGN, USD, GBP or EUR. Send fx_rate and fx_rate_date; we freeze the Naira value on the record, and tax and reports read that Naira value.
  • Lists are paginated. Use limit (1–100, default 20) and offset. Responses carry meta: { limit, offset, total }.
  • Dates are plain ISO days (YYYY-MM-DD). Timestamps are ISO 8601 in UTC.
  • Single records return { data: ... }; errors return { error: "message" }.
  • Retries are safe. Send an Idempotency-Key header on any write — see Idempotency. On transactions you can also supply your own id on create.

Idempotency

Networks drop. When a create or update times out you rarely know whether it landed. Send an Idempotency-Key header and you can retry the exact same call without risking a duplicate record in the books.

Optional, but recommended on every write

Send Idempotency-Key on POST, PUT, PATCH and DELETE. Any unique string works — a UUID or your own order reference. Requests without the header behave exactly as before.

Retries replay, they do not rewrite

The first call runs normally and we store its status and body for 24 hours against your API key. A retry with the same key and the same body returns that stored response with the header Idempotency-Replayed: true.

One key, one request

Reusing a key with a different body returns 409. Retrying while the first call is still running also returns 409 — wait a moment and try again.

Failures free the key

If we return a 5xx, the key is released so the same key can be retried safely. Keys expire after 24 hours.

Example
# Send the same key when you retry a create or update
curl -X POST https://bizbrada.com/api/public/v1/transactions \
  -H "Authorization: Bearer bb_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sale-2026-01-14-000431" \
  -d '{ "kind": "sale", "amount_minor": 250000, "occurred_on": "2026-01-14", "description": "Two cartons" }'

# A repeat of the same call returns the original response, with:
#   Idempotency-Replayed: true

Test sandbox

Every endpoint below is mirrored under /sandbox. Call it with no API key and it replies with the same sample response you see in these docs — so you can prove your client, your headers and your JSON parsing all work before you hold a real key, and without touching anybody's books.

  • Swap /api/public/v1 for /api/public/v1/sandbox in any path. Path ids can be anything — /sandbox/invoices/test works.
  • Nothing is read, written or billed, and no request counts against your rate limit.
  • Replies are wrapped in { sandbox: true, endpoint, response }. The real payload is in response; the live endpoint returns that shape on its own.
  • GET /api/public/v1/sandbox lists every path the sandbox answers.
Try it now
curl https://bizbrada.com/api/public/v1/sandbox/transactions

# Directory of everything the sandbox answers
curl https://bizbrada.com/api/public/v1/sandbox

Each endpoint card below has a Try it button that runs the sandbox call in your browser.

Health

Check that the API is reachable before you wire anything else up.

GET/api/public/v1/healthno key needed

Service status. No API key required.

Example request
curl https://bizbrada.com/api/public/v1/health
Response — 200
{
  "status": "ok",
  "service": "bizbrada-api",
  "version": "1.0.0"
}
GET /api/public/v1/sandbox/health

Transactions

Sales, expenses and purchases. Amounts are in minor units (kobo for Naira) and every record also stores its Naira value.

GET/api/public/v1/transactionsscope: read

List transactions, newest first by date recorded.

Query parameters

NameTypeNotes
limitinteger 1–100Page size. Defaults to 20.
offsetintegerRows to skip. Defaults to 0.
fromYYYY-MM-DDOnly records on or after this date.
toYYYY-MM-DDOnly records on or before this date.
Example request
curl "https://bizbrada.com/api/public/v1/transactions?limit=2&from=2026-01-01" \
  -H "Authorization: Bearer bb_YOUR_KEY"
Response — 200
{
  "data": [
    {
      "id": "0f0c3f2a-6d1e-4a0e-9a1b-8a3f5d2c1e77",
      "business_id": "9d2b1c44-1f4a-4a1d-8c9d-2e6b0f3a7b21",
      "kind": "sale",
      "amount_minor": 250000,
      "base_amount_minor": 250000,
      "currency": "NGN",
      "fx_rate": 1,
      "fx_rate_date": null,
      "occurred_on": "2026-01-14",
      "description": "Two cartons of tomato paste",
      "counterparty": "Mama Chidi Stores",
      "payment_method": "transfer",
      "account_id": null,
      "contact_id": null,
      "invoice_id": null,
      "tax_treatment": "vatable",
      "reconciliation_status": "unmatched",
      "source": "import",
      "created_at": "2026-01-14T09:12:44.120Z",
      "updated_at": "2026-01-14T09:12:44.120Z",
      "deleted_at": null
    }
  ],
  "meta": { "limit": 2, "offset": 0, "total": 148 }
}
GET /api/public/v1/sandbox/transactions
POST/api/public/v1/transactionsscope: write

Record one transaction. Send your own id (a UUID) to make the call idempotent — repeating it will not create a duplicate.

Body fields

NameTypeNotes
iduuidOptional. Your own id, used for idempotent retries.
kindrequiredsale | expense | purchase | otherWhat kind of record this is.
amount_minorrequiredintegerAmount in minor units, e.g. 250000 = ₦2,500.00.
currency3-letter codeNGN, USD, GBP or EUR. Defaults to NGN.
fx_ratenumberRate to Naira on the day. Defaults to 1.
fx_rate_dateYYYY-MM-DDDate the rate applies to.
occurred_onrequiredYYYY-MM-DDDate of the transaction.
descriptionrequiredstring ≤ 500What it was for.
counterpartystring ≤ 200Who you sold to or bought from.
payment_methodstring ≤ 100Cash, transfer, POS, etc.
account_iduuidChart of accounts entry.
contact_iduuidCustomer or supplier.
tax_treatmentvatable | exempt | zero_rated | wht_applicableDefaults to vatable.
sourcequick_entry | ocr | import | recurringDefaults to import.
Example request
curl -X POST https://bizbrada.com/api/public/v1/transactions \
  -H "Authorization: Bearer bb_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "sale",
    "amount_minor": 250000,
    "occurred_on": "2026-01-14",
    "description": "Two cartons of tomato paste",
    "counterparty": "Mama Chidi Stores"
  }'
Response — 201
{
  "data": {
    "id": "0f0c3f2a-6d1e-4a0e-9a1b-8a3f5d2c1e77",
    "kind": "sale",
    "amount_minor": 250000,
    "base_amount_minor": 250000,
    "currency": "NGN",
    "occurred_on": "2026-01-14",
    "description": "Two cartons of tomato paste",
    "tax_treatment": "vatable",
    "source": "import"
  }
}
POST /api/public/v1/sandbox/transactions
GET/api/public/v1/transactions/{id}scope: read

Fetch one transaction.

Example request
curl https://bizbrada.com/api/public/v1/transactions/0f0c3f2a-6d1e-4a0e-9a1b-8a3f5d2c1e77 \
  -H "Authorization: Bearer bb_YOUR_KEY"
Response — 200
{ "data": { "id": "0f0c3f2a-...", "kind": "sale", "amount_minor": 250000 } }
GET /api/public/v1/sandbox/transactions/sample-id
PATCH/api/public/v1/transactions/{id}scope: write

Change a transaction. Send only the fields you want changed. Changing amount, currency or rate recalculates the stored Naira value.

Body fields

NameTypeNotes
kindsale | expense | purchase | otherOptional.
amount_minorintegerOptional.
currency3-letter codeOptional.
fx_ratenumberOptional.
occurred_onYYYY-MM-DDOptional.
descriptionstring ≤ 500Optional.
tax_treatmentvatable | exempt | zero_rated | wht_applicableOptional.
Example request
curl -X PATCH https://bizbrada.com/api/public/v1/transactions/0f0c3f2a-6d1e-4a0e-9a1b-8a3f5d2c1e77 \
  -H "Authorization: Bearer bb_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "description": "Two cartons — corrected" }'
Response — 200
{ "data": { "id": "0f0c3f2a-...", "description": "Two cartons — corrected" } }
PATCH /api/public/v1/sandbox/transactions/sample-id
DELETE/api/public/v1/transactions/{id}scope: write

Remove a transaction. The record is kept in your audit trail.

Example request
curl -X DELETE https://bizbrada.com/api/public/v1/transactions/0f0c3f2a-6d1e-4a0e-9a1b-8a3f5d2c1e77 \
  -H "Authorization: Bearer bb_YOUR_KEY"
Response — 200
{ "deleted": true, "id": "0f0c3f2a-6d1e-4a0e-9a1b-8a3f5d2c1e77" }
DELETE /api/public/v1/sandbox/transactions/sample-id

Accounts

Your chart of accounts, read-only over the API.

GET/api/public/v1/accountsscope: read

List the chart of accounts.

Query parameters

NameTypeNotes
limitinteger 1–100Page size. Defaults to 20.
offsetintegerRows to skip. Defaults to 0.
Example request
curl "https://bizbrada.com/api/public/v1/accounts?limit=50" \
  -H "Authorization: Bearer bb_YOUR_KEY"
Response — 200
{
  "data": [
    {
      "id": "3a91f0b2-2c11-4c8f-9b6a-77c0d1c0aa10",
      "business_id": "9d2b1c44-...",
      "name": "Sales",
      "code": "4000",
      "type": "income",
      "is_default": true,
      "archived_at": null,
      "created_at": "2025-11-02T10:00:00.000Z",
      "updated_at": "2025-11-02T10:00:00.000Z"
    }
  ],
  "meta": { "limit": 50, "offset": 0, "total": 18 }
}
GET /api/public/v1/sandbox/accounts

Contacts

Customers and suppliers. Needed before you can raise an invoice.

GET/api/public/v1/contactsscope: read

List contacts alphabetically.

Query parameters

NameTypeNotes
kindcustomer | supplier | bothFilter by contact type.
limitinteger 1–100Page size. Defaults to 20.
offsetintegerRows to skip. Defaults to 0.
Example request
curl "https://bizbrada.com/api/public/v1/contacts?kind=customer" \
  -H "Authorization: Bearer bb_YOUR_KEY"
Response — 200
{
  "data": [
    {
      "id": "c4a1f0a9-11b2-4e6c-9d3a-0f7c2b8e1d54",
      "kind": "customer",
      "name": "Mama Chidi Stores",
      "email": "chidi@example.com",
      "phone": "+2348030000000",
      "address": "12 Balogun Street, Lagos",
      "tin": "12345678-0001",
      "is_corporate": false,
      "payee_class": "unincorporated"
    }
  ],
  "meta": { "limit": 20, "offset": 0, "total": 34 }
}
GET /api/public/v1/sandbox/contacts
POST/api/public/v1/contactsscope: write

Create a customer or supplier.

Body fields

NameTypeNotes
kindrequiredcustomer | supplier | bothContact type.
namerequiredstring ≤ 200Business or person name.
emailemailOptional.
phonestring ≤ 50Optional.
addressstring ≤ 500Optional.
tinstring ≤ 50Tax identification number, if you have it.
is_corporatebooleanDefaults to false.
payee_classcompany | individual | unincorporatedDrives WHT rates. Defaults to company.
Example request
curl -X POST https://bizbrada.com/api/public/v1/contacts \
  -H "Authorization: Bearer bb_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "customer", "name": "Mama Chidi Stores", "payee_class": "unincorporated" }'
Response — 201
{ "data": { "id": "c4a1f0a9-...", "kind": "customer", "name": "Mama Chidi Stores" } }
POST /api/public/v1/sandbox/contacts

Invoices

Invoices with line items. VAT and WHT are calculated server-side from the line items and the rate you set — you never post totals yourself.

GET/api/public/v1/invoicesscope: read

List invoices, newest issue date first.

Query parameters

NameTypeNotes
statusdraft | sent | part_paid | paid | cancelledFilter by status.
fromYYYY-MM-DDIssued on or after.
toYYYY-MM-DDIssued on or before.
limitinteger 1–100Page size. Defaults to 20.
offsetintegerRows to skip. Defaults to 0.
Example request
curl "https://bizbrada.com/api/public/v1/invoices?status=sent" \
  -H "Authorization: Bearer bb_YOUR_KEY"
Response — 200
{
  "data": [
    {
      "id": "b71c2f88-3e4a-4a10-8f2c-9c1e5a7d2b03",
      "number": "INV-0042",
      "seq": 42,
      "status": "sent",
      "contact_id": "c4a1f0a9-...",
      "issue_date": "2026-01-10",
      "due_date": "2026-01-24",
      "currency": "NGN",
      "fx_rate": 1,
      "subtotal_minor": 1000000,
      "vat_minor": 75000,
      "wht_minor": 50000,
      "total_minor": 1075000,
      "amount_due_minor": 1025000,
      "paid_minor": 0
    }
  ],
  "meta": { "limit": 20, "offset": 0, "total": 42 }
}
GET /api/public/v1/sandbox/invoices
POST/api/public/v1/invoicesscope: write

Create a draft invoice with its line items.

Body fields

NameTypeNotes
contact_idrequireduuidWho the invoice is for.
issue_daterequiredYYYY-MM-DDInvoice date.
due_dateYYYY-MM-DDOptional payment due date.
currency3-letter codeDefaults to NGN.
fx_ratenumberRate to Naira. Defaults to 1.
wht_ratenumber 0–100Withholding tax rate to deduct. Defaults to 0.
notesstring ≤ 2000Shown on the invoice.
itemsrequiredarray (1–100)description, quantity, unit_price_minor, vat_rate, account_id.
Example request
curl -X POST https://bizbrada.com/api/public/v1/invoices \
  -H "Authorization: Bearer bb_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "c4a1f0a9-11b2-4e6c-9d3a-0f7c2b8e1d54",
    "issue_date": "2026-01-10",
    "due_date": "2026-01-24",
    "wht_rate": 5,
    "items": [
      { "description": "Consulting — January", "quantity": 1, "unit_price_minor": 1000000, "vat_rate": 7.5 }
    ]
  }'
Response — 201
{
  "data": {
    "id": "b71c2f88-...",
    "number": "INV-0042",
    "status": "draft",
    "subtotal_minor": 1000000,
    "vat_minor": 75000,
    "wht_minor": 50000,
    "total_minor": 1075000,
    "amount_due_minor": 1025000
  }
}
POST /api/public/v1/sandbox/invoices
GET/api/public/v1/invoices/{id}scope: read

Fetch one invoice with its line items.

Example request
curl https://bizbrada.com/api/public/v1/invoices/b71c2f88-3e4a-4a10-8f2c-9c1e5a7d2b03 \
  -H "Authorization: Bearer bb_YOUR_KEY"
Response — 200
{
  "data": {
    "id": "b71c2f88-...",
    "number": "INV-0042",
    "status": "sent",
    "items": [
      {
        "id": "e2d1...",
        "description": "Consulting — January",
        "quantity": 1,
        "unit_price_minor": 1000000,
        "vat_rate": 7.5,
        "line_total_minor": 1000000
      }
    ]
  }
}
GET /api/public/v1/sandbox/invoices/sample-id
PATCH/api/public/v1/invoices/{id}scope: write

Change status, due date or notes.

Body fields

NameTypeNotes
statusdraft | sent | part_paid | paid | cancelledOptional.
due_dateYYYY-MM-DDOptional.
notesstring ≤ 2000Optional.
Example request
curl -X PATCH https://bizbrada.com/api/public/v1/invoices/b71c2f88-3e4a-4a10-8f2c-9c1e5a7d2b03 \
  -H "Authorization: Bearer bb_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "paid" }'
Response — 200
{ "data": { "id": "b71c2f88-...", "status": "paid" } }
PATCH /api/public/v1/sandbox/invoices/sample-id
DELETE/api/public/v1/invoices/{id}scope: write

Cancel an invoice. Invoices are never hard-deleted — the number stays in your books.

Example request
curl -X DELETE https://bizbrada.com/api/public/v1/invoices/b71c2f88-3e4a-4a10-8f2c-9c1e5a7d2b03 \
  -H "Authorization: Bearer bb_YOUR_KEY"
Response — 200
{ "ok": true }
DELETE /api/public/v1/sandbox/invoices/sample-id

Response schemas

The shape of each object the API returns. Fields marked | null may be absent.

Webhooks

Register an HTTPS endpoint in Settings → API and choose the events you want. We show the signing secret once; keep it on your server.

EventSent when
transaction.createdA new record enters the books.
transaction.updatedAn existing record is changed.
invoice.createdAn invoice is raised.
invoice.updatedStatus, due date or notes change.
invoice.paidAn invoice is fully settled.
filing.status_changedA return moves through preparation, review or filing.

Each delivery is a POST with a JSON body and an x-bizbrada-signature header: an HMAC-SHA256 of the raw body, hex encoded, using your signing secret. Verify it with a constant-time compare before you trust the payload. Reply 2xx quickly; anything else is retried with backoff.

Verifying a delivery (Node)
import { createHmac, timingSafeEqual } from "crypto";

// Raw body — verify before JSON.parse.
const signature = req.headers["x-bizbrada-signature"];
const expected = createHmac("sha256", process.env.BIZBRADA_WEBHOOK_SECRET)
  .update(rawBody)
  .digest("hex");

const ok =
  signature.length === expected.length &&
  timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

if (!ok) return res.status(401).send("Invalid signature");

Webhooks we receive

Payment callbacks come in on the same versioned base. GET /api/public/v1/webhooks lists every receiver and the exact URL to paste into a provider dashboard. Card payments are handled at POST /api/public/v1/webhooks/flutterwave, which checks the verif-hash header and then re-verifies the transaction with the provider before anything is applied. The older /api/public/hooks/flutterwave path still answers.

To confirm your wiring, send anything to /api/public/v1/webhooks/test. It echoes the method, the body it read and whether your verif-hash header would have been accepted — without reading or writing a single row.

Testing a webhook delivery
curl -X POST https://bizbrada.com/api/public/v1/webhooks/test \
  -H "content-type: application/json" \
  -H "verif-hash: $FLUTTERWAVE_WEBHOOK_HASH" \
  -d '{"event":"charge.completed","data":{"tx_ref":"test-ref"}}'

Errors

Errors always come back as JSON with a single error string. We never leak another business's data in an error message.

400The request body or a parameter did not validate.
{ "error": "Invalid transaction: amount_minor is required" }
401Missing, malformed, revoked or expired API key.
{ "error": "Missing or invalid API key" }
403Key lacks the scope, or the plan does not include API access.
{ "error": "Insufficient scope. This endpoint requires the 'write' scope." }
404No such record in this business.
{ "error": "Resource not found" }
429Rate limit exceeded. Retry after the Retry-After header.
{ "error": "Rate limit exceeded. Slow down or increase your rate limit in Settings > API." }
500Something failed on our side. Safe to retry.
{ "error": "Internal server error" }

Ready to build?

Create a key in Settings → API and make your first call in under five minutes. Growth adds the API as a paid add-on; Scale includes it.