Bunya REST API
The Bunya REST API lets you create and manage invoices programmatically. It is available to organizations on the Business or Enterprise plan.
https://yourdomain.com/api/v1Authentication
All requests must be authenticated with an API key. Generate one from Settings → API Keys in your dashboard. Keys are prefixed bunya_ and are shown only once at creation — store them securely.
Pass your key in one of two headers:
| Header | Example |
|---|---|
Authorization | Bearer bunya_abc123... |
X-API-Key | bunya_abc123... |
curl https://yourdomain.com/api/v1/invoices \
-H "Authorization: Bearer bunya_abc123..."Errors
Errors are returned as JSON with an error field.
{ "error": "invoice not found" }| Status | Meaning |
|---|---|
400 | Invalid request body or parameters |
401 | Missing or invalid API key |
403 | Plan does not include API access |
404 | Resource not found |
500 | Internal server error |
List invoices
/api/v1/invoicesReturns all invoices for your organization, newest first.
curl https://yourdomain.com/api/v1/invoices \
-H "Authorization: Bearer bunya_abc123..."[
{
"uid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"invoice_number": "INV-0001",
"status": "sent",
"currency": "USD",
"date_created": "2026-04-01",
"date_due": "2026-04-15",
"to_name": "Acme Corp",
"to_address": "123 Main St, Springfield",
"to_email": "billing@acme.example",
"total_amount": "250.00",
"payment_link_url": "https://checkout.stripe.com/...",
"lines": [
{
"description": "Web design services",
"quantity": "5.00",
"unit_price": "50.00",
"amount": "250.00"
}
]
}
]Create invoice
/api/v1/invoicesCreates a new draft invoice for your organization. 1 token is deducted from your balance on creation.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
currency | string | yes | ISO 4217 code, e.g. USD |
date_created | string | yes | Invoice date — YYYY-MM-DD |
date_due | string | yes | Due date — YYYY-MM-DD |
to_name | string | yes | Client / recipient name |
to_address | string | no | Client address |
to_phone | string | no | Client phone number |
to_email | string | no | Client email address |
memo | string | no | Internal memo (not shown on invoice) |
description | string | no | Invoice-level notes shown to the client |
lines | array | yes | One or more line items (see below) |
Line item fields
| Field | Type | Required | Description |
|---|---|---|---|
description | string | yes | Line item description |
quantity | decimal | yes | Quantity, e.g. 5.00 |
unit_price | decimal | yes | Price per unit, e.g. 50.00 |
curl -X POST https://yourdomain.com/api/v1/invoices \
-H "Authorization: Bearer bunya_abc123..." \
-H "Content-Type: application/json" \
-d '{
"currency": "USD",
"date_created": "2026-04-10",
"date_due": "2026-04-24",
"to_name": "Acme Corp",
"to_email": "billing@acme.example",
"lines": [
{
"description": "Web design services",
"quantity": "5",
"unit_price": "50.00"
}
]
}'{
"uid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"invoice_number": "INV-0002",
"status": "draft",
"currency": "USD",
"date_created": "2026-04-10",
"date_due": "2026-04-24",
"to_name": "Acme Corp",
"to_address": null,
"to_email": "billing@acme.example",
"total_amount": "250.00",
"payment_link_url": null,
"lines": [
{
"description": "Web design services",
"quantity": "5",
"unit_price": "50.00",
"amount": "250.00"
}
]
}Get invoice
/api/v1/invoices/{uid}Fetches a single invoice by its UUID.
curl https://yourdomain.com/api/v1/invoices/3fa85f64-5717-4562-b3fc-2c963f66afa6 \
-H "Authorization: Bearer bunya_abc123..."{
"uid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"invoice_number": "INV-0001",
"status": "paid",
"currency": "USD",
"date_created": "2026-04-01",
"date_due": "2026-04-15",
"to_name": "Acme Corp",
"to_address": "123 Main St, Springfield",
"to_email": "billing@acme.example",
"total_amount": "250.00",
"payment_link_url": "https://checkout.stripe.com/...",
"lines": [
{
"description": "Web design services",
"quantity": "5.00",
"unit_price": "50.00",
"amount": "250.00"
}
]
}Webhooks
Webhooks let Bunya notify your application in real time when invoice events occur — no polling required. Register an endpoint URL and Bunya will send a signed HTTP POST request each time a subscribed event fires.
Manage endpoints from Settings → Webhooks in your dashboard. Each endpoint gets its own signing secret, shown once at creation.
Events
Subscribe to one or more events when creating an endpoint. More event types will be added in future releases.
| Event | Fired when |
|---|---|
invoice.finalized | An invoice is finalized (locked for payment) |
invoice.paid | An invoice is marked paid — via Stripe, PayPal, or manually |
Payload
Requests are sent as JSON with Content-Type: application/json. Two headers identify the request:
| Header | Value |
|---|---|
X-Bunya-Event | The event name, e.g. invoice.paid |
X-Bunya-Signature | HMAC-SHA256 signature — see Verifying signatures |
invoice.finalized
{
"event": "invoice.finalized",
"bunya_version": "1",
"occurred_at": "2026-04-28T09:00:00Z",
"data": {
"invoice_uid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"invoice_number": "INV-0001",
"organization_uid": "a1b2c3d4-...",
"total_amount": "250.00",
"currency": "USD"
}
}invoice.paid
{
"event": "invoice.paid",
"bunya_version": "1",
"occurred_at": "2026-04-28T09:15:00Z",
"data": {
"invoice_uid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"invoice_number": "INV-0001",
"organization_uid": "a1b2c3d4-...",
"client_name": "Acme Corp",
"total_amount": "250.00",
"currency": "USD",
"paid_at": "2026-04-28T09:15:00Z",
"payment_method": "stripe"
}
}Verifying signatures
Every request includes an X-Bunya-Signature header of the form sha256=<hex>. Compute HMAC-SHA256(secret, raw_request_body) and compare it to the header value after stripping the sha256= prefix. Always use a constant-time comparison to prevent timing attacks.
const crypto = require("crypto");
function verifySignature(secret, rawBody, signatureHeader) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const received = signatureHeader.replace("sha256=", "");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(received, "hex")
);
}
// Express example
app.post("/webhooks/bunya", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-bunya-signature"];
if (!verifySignature(process.env.BUNYA_WEBHOOK_SECRET, req.body, sig)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
if (event.event === "invoice.finalized") {
// handle finalization
} else if (event.event === "invoice.paid") {
// handle payment
}
res.sendStatus(200);
});import hashlib, hmac
def verify_signature(secret: str, raw_body: bytes, signature_header: str) -> bool:
expected = hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
received = signature_header.removeprefix("sha256=")
return hmac.compare_digest(expected, received)
# Flask example
@app.post("/webhooks/bunya")
def bunya_webhook():
sig = request.headers.get("X-Bunya-Signature", "")
if not verify_signature(os.environ["BUNYA_WEBHOOK_SECRET"], request.get_data(), sig):
abort(401)
event = request.get_json()
if event["event"] == "invoice.finalized":
pass # handle finalization
elif event["event"] == "invoice.paid":
pass # handle payment
return "", 200Your endpoint must return a 2xx status within 30 seconds. Any other status or a timeout counts as a failure and triggers a retry.