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.

Base URLhttps://yourdomain.com/api/v1

Authentication

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:

HeaderExample
AuthorizationBearer bunya_abc123...
X-API-Keybunya_abc123...
cURL
curl https://yourdomain.com/api/v1/invoices \
  -H "Authorization: Bearer bunya_abc123..."

Errors

Errors are returned as JSON with an error field.

Error response
{ "error": "invoice not found" }
StatusMeaning
400Invalid request body or parameters
401Missing or invalid API key
403Plan does not include API access
404Resource not found
500Internal server error

List invoices

GET/api/v1/invoices

Returns all invoices for your organization, newest first.

cURL
curl https://yourdomain.com/api/v1/invoices \
  -H "Authorization: Bearer bunya_abc123..."
Response 200 OK
[
  {
    "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

POST/api/v1/invoices

Creates a new draft invoice for your organization. 1 token is deducted from your balance on creation.

Request body

FieldTypeRequiredDescription
currencystringyesISO 4217 code, e.g. USD
date_createdstringyesInvoice date — YYYY-MM-DD
date_duestringyesDue date — YYYY-MM-DD
to_namestringyesClient / recipient name
to_addressstringnoClient address
to_phonestringnoClient phone number
to_emailstringnoClient email address
memostringnoInternal memo (not shown on invoice)
descriptionstringnoInvoice-level notes shown to the client
linesarrayyesOne or more line items (see below)

Line item fields

FieldTypeRequiredDescription
descriptionstringyesLine item description
quantitydecimalyesQuantity, e.g. 5.00
unit_pricedecimalyesPrice per unit, e.g. 50.00
cURL
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"
      }
    ]
  }'
Response 201 Created
{
  "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

GET/api/v1/invoices/{uid}

Fetches a single invoice by its UUID.

cURL
curl https://yourdomain.com/api/v1/invoices/3fa85f64-5717-4562-b3fc-2c963f66afa6 \
  -H "Authorization: Bearer bunya_abc123..."
Response 200 OK
{
  "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.

RetriesFailed deliveries are retried up to 5 times with exponential backoff: 1 min → 5 min → 30 min → 2 hr → 8 hr. You can also trigger a manual retry from the delivery log.

Events

Subscribe to one or more events when creating an endpoint. More event types will be added in future releases.

EventFired when
invoice.finalizedAn invoice is finalized (locked for payment)
invoice.paidAn 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:

HeaderValue
X-Bunya-EventThe event name, e.g. invoice.paid
X-Bunya-SignatureHMAC-SHA256 signature — see Verifying signatures

invoice.finalized

Payload
{
  "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

Payload
{
  "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.

Node.js
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);
});
Python
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 "", 200

Your endpoint must return a 2xx status within 30 seconds. Any other status or a timeout counts as a failure and triggers a retry.