Skip to content
ClientBilling

Updated September 18, 2026

API reference

For scripts and agents that run an invoicing account on its owner's behalf. Create a key under Settings, API keys in the app. Machine-readable OpenAPI is at /docs/api/openapi.json.

Affiliate disclosure: ClientBilling may earn a commission from CDG Commerce. Details

For scripts and agents operating a merchant's account with the merchant's consent (decisions 0017, 0025). Base URL https://www.clientbilling.com. All requests carry Authorization: Bearer cb_live_..., created at /app/settings/api. A key acts as the account that created it. 120 requests per key per minute. Errors are { "error": "..." } with a meaningful status.

Money is integer cents. Dates are ISO 8601. status is one of draft, sent, viewed, overdue, paid, void.

Idempotency and actors

Every state-changing request (POST, DELETE) under /api/v1 must send Idempotency-Key: <opaque string up to 256 chars>. The same key with the same method, path, and body returns the original response with Idempotency-Replayed: true. The same key with a different body returns 409. Keys are remembered for 24 hours per account.

Successful mutations include provenance:

{
  "actor": { "type": "api_key", "id": "cmu...", "authorizationId": "cmu..." },
  "request": { "idempotencyKey": "create-1001" }
}

Material mutations also append a hashed BillingEvent and, when the invoice projection changes, an InvoiceVersion.

Scopes and service accounts

Keys carry scopes (decision 0026). Missing a required scope returns 403. The set is: invoice:read, invoice:write, invoice:send, invoice:void, reminder:send, payment:read, payment:record, cost:read, proof:read. Bind a key to a service account under Settings, API keys when the actor on mutations should be that service account rather than the key itself.

OpenAPI 3.1: GET /docs/api/openapi.json.

Live and test keys

Keys come in two kinds. Live keys use the cb_live_ prefix; test keys use cb_test_. Both authenticate the same way and are held to 120 requests a minute.

A test key adds a write allowance of 100 a day per account, shared across REST and MCP, and returns 429 past it.

A test key acts on your real account: it creates real invoices and records real payments against them. There is no separate test dataset today. It cannot send email: POST /api/v1/invoices/:id/send, POST /api/v1/invoices/:id/remind, and the send_invoice and send_reminder MCP tools all return 403 for a cb_test_ key, because a send would reach the real client on file (decision 0028). Use a live key to test the send path.

Webhooks

Register an HTTPS endpoint under Settings, API keys. Each delivery POSTs JSON:

{
  "id": "evt_...",
  "type": "invoice.sent",
  "occurredAt": "2026-09-19T12:00:00.000Z",
  "organizationId": "userId",
  "data": { "invoiceId": "..." },
  "actor": { "type": "service_account", "id": "...", "authorizationId": "..." },
  "proofStatus": "pending"
}

Header ClientBilling-Signature: t=<unix>,v1=<hmac_sha256_hex> signs ${t}.${rawBody} with the endpoint secret. Reject timestamps older than five minutes. Event types: invoice.created, invoice.sent, invoice.viewed, invoice.paid, payment.recorded, invoice.overdue. Delivery is at-least-once with retries.

Clients

GET /api/v1/clients lists clients.

POST /api/v1/clients creates one.

{ "name": "Jane Client", "email": "jane@example.com", "company": "Client Co", "phone": "555-0100" }

Returns 201 { "client": { "id", "name", "email", "phone", "company", "createdAt" } }.

Invoices

GET /api/v1/invoices?status=overdue lists invoices, newest first, optionally filtered by status.

POST /api/v1/invoices creates a draft. Give either clientId or newClient.

{
  "newClient": { "name": "Jane Client", "email": "jane@example.com" },
  "lines": [
    { "description": "Water heater replacement, labor", "quantity": 4, "unitPrice": 125 },
    { "description": "50 gal water heater unit", "quantity": 1, "unitPrice": "750.00" }
  ],
  "taxRate": 7,
  "dueDate": "2026-10-02",
  "notes": "Net 14."
}

unitPrice is in dollars (number or string); it is stored as cents. taxRate is a percent. Returns 201 { "invoice": {...} } with publicUrl and pdfUrl.

GET /api/v1/invoices/:id returns one invoice.

POST /api/v1/invoices/:id/status with { "status": "sent" | "paid" | "void", "reason"?: "..." }. Void requires a non-empty reason (max 500 characters); 400 without it. Only allowed transitions succeed; 409 otherwise.

POST /api/v1/invoices/:id/send emails the invoice, with the PDF attached, to the client on file. 400 if the client has no email, 429 past 20 emails per hour, 503 if email is not enabled on the deployment. On success the invoice becomes sent.

GET /api/v1/invoices/:id/payments lists payments recorded against the invoice, newest first.

POST /api/v1/invoices/:id/payments records money received (decision 0019). Body: { "amountCents": 12500, "method": "bank_transfer", "paidOn": "2026-09-18", "note": "Deposit" }. method is one of cash, check, bank_transfer, card, other (default other); paidOn defaults to now and cannot be in the future. Paying the full balance flips the invoice to paid. 400 if the amount is zero or exceeds balanceCents, 409 on void or already paid invoices. Returns 201 { "payment": {...}, "invoice": {...} }.

DELETE /api/v1/invoices/:id/payments/:paymentId removes a payment record. A paid invoice reopens as overdue, viewed, sent, or draft depending on its history.

POST /api/v1/invoices/:id/status with { "status": "paid" } records one payment for the remaining balance with method other, so payment records stay the source of truth.

POST /api/v1/invoices/:id/remind sends one reminder to the client on file. 429 within 24 hours of the last reminder.

Invoice object

{
  "id": "cmu...", "number": "1001", "publicId": "abc123def456",
  "publicUrl": "https://www.clientbilling.com/i/abc123def456",
  "pdfUrl": "https://www.clientbilling.com/i/abc123def456/pdf",
  "status": "sent",
  "client": { "id": "cmu...", "name": "Jane Client", "email": "jane@example.com" },
  "issueDate": "2026-09-18T00:00:00.000Z", "dueDate": "2026-10-02T00:00:00.000Z",
  "currency": "USD", "taxRateBps": 700,
  "subtotalCents": 125000, "taxCents": 8750, "totalCents": 133750,
  "paidCents": 50000, "balanceCents": 83750,
  "notes": "Net 14.",
  "lineItems": [{ "id": "cmu...", "description": "...", "quantity": 4, "unitPriceCents": 12500 }],
  "sentAt": "...", "viewedAt": null, "paidAt": null, "voidedAt": null, "voidReason": null,
  "createdAt": "...", "updatedAt": "..."
}

Examples

export CB_KEY=cb_live_...
curl -s https://www.clientbilling.com/api/v1/invoices?status=overdue -H "Authorization: Bearer $CB_KEY"

curl -s -X POST https://www.clientbilling.com/api/v1/invoices \
  -H "Authorization: Bearer $CB_KEY" -H "Content-Type: application/json" \
  -d '{"newClient":{"name":"Jane Client","email":"jane@example.com"},"lines":[{"description":"Consulting, September","quantity":10,"unitPrice":150}],"taxRate":0,"dueDate":"2026-10-15"}'

curl -s -X POST https://www.clientbilling.com/api/v1/invoices/INVOICE_ID/send -H "Authorization: Bearer $CB_KEY"

curl -s -X POST https://www.clientbilling.com/api/v1/invoices/INVOICE_ID/payments \
  -H "Authorization: Bearer $CB_KEY" -H "Content-Type: application/json" \
  -d '{"amountCents":50000,"method":"check","paidOn":"2026-09-18","note":"Check 1042"}'

MCP

POST/GET https://www.clientbilling.com/api/mcp with the same Bearer key. Tools mirror the REST surface: list/get/create invoice, send, remind, record payment, get payment costs, verify record (planned). Write tools take idempotencyKey. Agent landing: /developers/invoicing-api-for-agents.

A refused call comes back with isError: true and the reason in the body: a missing scope, an exhausted test-key allowance, or an idempotency conflict. Check the flag, not just the body. scripts/test-mcp-deny.ts (npm run test:mcp <origin> <zero-scope-key>) holds that contract; it needs a running server and a live key with no scopes.

Not in v1 yet

Updating or deleting invoices and clients, team access, Machine-tier enforcement.