Billery API
Create and send invoices, quotes and receipts from your own app, website, booking system or AI agent. Billery renders the PDF, emails the customer and keeps the document in your account alongside everything you create by hand.
Customers can pay by bank transfer, by card through your own Stripe account, or in USDC straight to your own wallet, and other AI agents can pay an invoice over x402. Billery never holds the money: it goes directly to you.
Getting started
Every endpoint lives under https://app.billery.io/api/v1. Requests and responses are JSON, and all field names are snake_case.
Create a key in Settings → Developers, then send your first invoice:
curl https://app.billery.io/api/v1/invoices \
-H "Authorization: Bearer blry_test_..." \
-H "Content-Type: application/json" \
-d '{
"customer": { "name": "Ada Okafor", "email": "ada@example.com" },
"items": [
{ "description": "Boiler service", "quantity": 1, "unit_price": 120 }
],
"due_in_days": 14,
"send": true
}'Totals are always calculated by Billery from the line items, tax rate and discount — you never send a total, so your numbers and ours can't drift apart.
Authentication
Pass your key as a bearer token on every request. Keys carry the full permissions of your account, so keep them on your server — never in a browser, mobile app or public repo.
Authorization: Bearer blry_live_7f3c...Agent keys are the exception: they only carry their agent's rules (see Agents). A key is shown once, when you create it. We store only a hash, so a lost key can't be recovered — create a new one and revoke the old. Revoking takes effect immediately.
Use with Claude
Connect Billery to Claude and you can create and send invoices, quotes and receipts just by asking. There's no key to copy: you sign in to Billery once and approve the connection.
Connecting
- In Claude, open Customize → Connectors, click + and choose Add custom connector.
- Name it Billery, enter
https://app.billery.io/mcpas the URL and click Add. Leave the advanced settings empty. - Click Connect, sign in to Billery and approve access. You're sent back to Claude.
- In a chat, make sure Billery is turned on in the tools menu, then just ask.
On a Team or Enterprise plan, an owner adds the connector once under Organization settings → Connectors, and each member then connects their own Billery account from Customize → Connectors. Once connected on the web, Billery also works in Claude's desktop and mobile apps.
In Claude Code, add it from the terminal and sign in with /mcp:
claude mcp add --transport http billery https://app.billery.io/mcpWhat you can ask
- “Invoice Riverside Café £450 for October's website maintenance, due in 14 days.”
- “Make a quote for Sam Patel: 3 days of consulting at £600 a day plus 20% VAT.”
- “Make a receipt for the £120 Sam Patel paid today.”
- “What have I invoiced this month?”
- “Email the last invoice to the customer.”
Claude can find and add customers, create invoices, quotes and receipts, list your recent documents, get a link to share, and email a document to your customer. New documents start as drafts in your Billery account, where you can review or edit them, and Claude always asks before it emails anyone. Claude can't take or send payments: customers pay you directly using the payment details on the invoice.
Disconnecting and data
Disconnect at any time from Customize → Connectors in Claude. Billery only sees what Claude sends to its tools (such as customer names, line items and amounts) and never your conversations. See the privacy policy for details, or contact support.
Agents
In beta. Request access on the Agents page in Billery.
An agent is an AI or automation that works in your account inside rules you set. Create one on the Agents page and you get an agent key (blry_live_…). It works everywhere a normal key does, but within the agent's rules instead of with full access:
| Field | Type | Description |
|---|---|---|
| May create | rule | Whether it can add customers, invoices, quotes, receipts and usage. |
| Sending | rule | Automatic, only with your approval, or never. |
| Automatic limit | rule | Sends above this amount wait for approval, e.g. anything over $500. |
| Daily limit | rule | Sends beyond this many a day wait for approval. |
| Expires | rule | The key stops working after this date. |
A send that needs approval doesn't fail: the document is created, nothing goes to the customer, and you get a notification to approve or reject it in Billery. The response says so, and the agent should tell its user:
{
"object": "invoice",
"number": "INV-0019",
"status": "draft",
"send_status": "pending_approval",
"approval_id": "cmvqtgrxg000k..."
}Anything the agent isn't allowed to do at all returns 403 permission_denied with the reason. Every action an agent takes is listed in its activity log.
Connecting over MCP
AI tools that speak the Model Context Protocol can connect to Billery directly, with no code. Point them at the MCP server and pass the agent key as a bearer token:
URL: https://app.billery.io/mcp
Authorization: Bearer blry_live_...The agent gets these tools: search_customers, create_customer, create_invoice, create_quote, create_receipt, list_recent_documents, send_document, get_share_link, record_usage, list_usage and invoice_usage. Claude (see Use with Claude) and ChatGPT connect with sign-in instead of a key, and appear on your Agents page the same way.
Test mode
Keys come in two modes. A blry_test_ key validates and prices a request exactly like a live one and returns a realistic response, but writes nothing and emails nobody. Build the whole integration against test keys, then swap in a blry_live_key when you're ready.
Test responses carry "livemode": false and placeholder ids and numbers, so don't store them as real references.
Rate limits
Each key may make 120 requests per minute. Successful responses — and the 429 itself — tell you where you stand:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1790000040X-RateLimit-Reset is a Unix timestamp in seconds. Go over and you get a 429 with a Retry-Afterheader in seconds — wait that long and retry rather than looping immediately. Limits apply per key, so a busy background job won't starve your checkout flow if you give them separate keys.
Idempotency
Send an Idempotency-Key header on any create request. If the same key arrives again — a retry after a timeout, a double-submit, a queue redelivery — we replay the original response instead of creating a second document, and mark it with Idempotent-Replay: true.
-H "Idempotency-Key: booking-4821-invoice"Use something derived from your own records, like an order id. A request that fails releases its key, so you can safely retry with the same one. A request still in flight returns 409 conflict.
Errors
Failures use standard status codes and always have the same body shape:
{
"error": {
"type": "invalid_request",
"message": "At least one item is required",
"param": "items"
}
}| Field | Type | Description |
|---|---|---|
| authentication_error | 401 | Missing, invalid or revoked key. |
| invalid_request | 400 | Malformed JSON or a field that failed validation. Check param. |
| permission_denied | 403 | An agent key tried something its rules don't allow. The message says why. |
| not_found | 404 | No such document or customer on your account. |
| limit_reached | 402 | Free document allowance used up — upgrade to Pro. |
| conflict | 409 | A request with this Idempotency-Key is still processing. |
| rate_limited | 429 | Too many requests. Wait for Retry-After. |
| server_error | 500 | Something broke on our side. Safe to retry with the same idempotency key. |
Invoices
/api/v1/invoicesCreates an invoice. Pass send: true to email the PDF to the customer immediately; otherwise it lands in your account as a draft.
| Field | Type | Description |
|---|---|---|
| customer_id | string | Id of an existing customer. Use this or customer, not both. |
| customer | object | New or matched customer: name and email required; phone, address, city, country optional. Matched on email. |
| items | array | One to 100 line items, each with description, quantity and unit_price. |
| currency | string | Three-letter code. Defaults to the customer's currency, then GBP. |
| issue_date | string | YYYY-MM-DD. Defaults to today. |
| due_date | string | YYYY-MM-DD. Must not be before issue_date. |
| due_in_days | integer | Days from the issue date. Defaults to 14. Ignored if due_date is set. |
| payment_methods | array | How the customer may pay: any of bank, card, usdc. Defaults to every method you've set up for the currency. |
| tax_rate | number | Percentage from 0 to 100. Applied after the discount. |
| discount | number | Flat amount off the subtotal. Cannot exceed it. |
| notes | string | Free text printed on the document. |
| send | boolean | Email the document to the customer as a PDF straight away. With an agent key this follows the agent's sending rules. |
{
"object": "invoice",
"id": "clx8f2a0b0000...",
"livemode": true,
"number": "INV-0009",
"status": "sent",
"currency": "GBP",
"issue_date": "2026-09-23",
"due_date": "2026-10-07",
"customer": { "id": "clx...", "name": "Ada Okafor", "email": "ada@example.com" },
"items": [
{ "description": "Boiler service", "quantity": 1, "unit_price": 120, "amount": 120 }
],
"subtotal": 120,
"discount": 0,
"tax_rate": 0,
"tax_amount": 0,
"total": 120,
"notes": null,
"payment_methods": ["bank", "card"],
"payment_method": null,
"url": "https://app.billery.io/i/8f2c...",
"pdf_url": "https://app.billery.io/i/8f2c.../pdf",
"json_url": "https://app.billery.io/api/pay/8f2c...",
"x402_url": "https://app.billery.io/api/pay/8f2c.../x402",
"sent_at": "2026-09-23T09:14:22.104Z",
"paid_at": null,
"created_at": "2026-09-23T09:14:20.881Z"
}url is a shareable page your customer can open without signing in, and pdf_url downloads the same document. Both are safe to put in your own emails or WhatsApp messages. payment_method says how a paid invoice was paid (Card, USDC, Bank transfer, …); json_url and x402_url are for other agents (see Agent payments).
/api/v1/invoices/{id}Retrieves an invoice, including its current status and payment date.
Quotes
/api/v1/quotesSame shape as an invoice, with an expiry instead of a due date. Accepted quotes can be converted to an invoice from your dashboard.
| Field | Type | Description |
|---|---|---|
| expiry_date | string | YYYY-MM-DD. When the quote stops being valid. |
| valid_for_days | integer | Days from the issue date. Defaults to 30. Ignored if expiry_date is set. |
Everything else matches the invoice fields above. Responses carry "object": "quote", expiry_date and converted_invoice_id.
/api/v1/quotes/{id}Receipts
/api/v1/receiptsA receipt records money already taken — useful if you collect payment at the point of sale and only need to send proof. It's created as paid, so there's no due date and no chasing.
| Field | Type | Description |
|---|---|---|
| paid_at | string | YYYY-MM-DD. When payment was taken. Defaults to today. |
| payment_method | string | Printed on the receipt, e.g. "Card" or "Bank transfer". |
Receipts don't count towards the free document allowance, and the response comes back with "object": "receipt". Receipts are API-only for now — they don't appear as a create option in the dashboard.
Usage billing
In beta. Request access on the Agents page in Billery.
For work billed by the unit (tickets resolved, reports written, API calls), record each piece as it happens and invoice it later. The same description at the same price becomes one line on the invoice.
/api/v1/usage| Field | Type | Description |
|---|---|---|
| customer_id | string | The customer who used it. |
| description | string | As it should read on the invoice, e.g. "Support tickets resolved". |
| quantity | number | How many units. |
| unit_price | number | Price per unit. |
| currency | string | Defaults to the customer's currency. |
| occurred_at | string | ISO 8601 time it happened. Defaults to now. |
| id | string | Your own id for the event. Sending the same id again returns the first event (200) instead of recording it twice. |
| metadata | object | Anything you want to keep with the event. |
curl https://app.billery.io/api/v1/usage \
-H "Authorization: Bearer blry_live_..." \
-H "Content-Type: application/json" \
-d '{
"customer_id": "clx...",
"description": "Support tickets resolved",
"quantity": 1,
"unit_price": 2,
"id": "ticket-4821"
}'/api/v1/usageLists events, newest first. Filter with customer_id and status (unbilled, billed or all); page with limit (up to 100) and offset.
/api/v1/usage/invoiceRolls a customer's unbilled usage into a draft invoice and marks it billed. Takes customer_id, plus optional through (bill only up to this ISO 8601 time, e.g. the end of last month), currency (needed if the customer has usage in more than one), due_date, tax_rate, notes, payment_methods and send. The response is the invoice plus usage_events_billed.
You don't have to call it: on the 1st of each month Billery drafts every customer's unbilled usage from earlier months into invoices and lets you know they're ready to review. Nothing is sent to customers until you, or an agent within its rules, send it.
Agent payments (x402)
In beta, and not yet available on live invoices.
Every invoice link works for agents too, so an AI paying on behalf of your customer can read and settle it without a person. Ask the link for JSON and you get the invoice with every way to pay it:
curl https://app.billery.io/i/8f2c... -H "Accept: application/json"{
"object": "invoice",
"number": "INV-0016",
"status": "sent",
"currency": "USD",
"total": 0.5,
"amount_due": 0.5,
"payment_methods": [
{
"type": "usdc",
"protocol": "x402",
"url": "https://app.billery.io/api/pay/8f2c.../x402",
"network": "eip155:8453",
"asset": "0x8335...2913",
"pay_to": "0x1AC5...c848",
"amount": "500000"
},
{ "type": "bank_transfer", "details": "...", "reference": "INV-0016" }
]
}The USDC method follows the x402 standard (version 2, exact scheme), so any x402 client can pay it:
- Request the
url. It answers402 Payment Requiredwith the requirements in thePAYMENT-REQUIREDheader (and the body). - Sign an EIP-3009 USDC transfer authorisation for exactly
amounttopayTo, the business's own wallet. - Repeat the request with it in the
PAYMENT-SIGNATUREheader. The payment is settled on chain, the invoice is marked paid, and you get200with the transaction and aPAYMENT-RESPONSEheader.
{
"status": "paid",
"invoice": "INV-0016",
"transaction": "0x688e...4070",
"explorer_url": "https://basescan.org/tx/0x688e...4070"
}The payer needs USDC but no ETH: the settlement service pays the network fee. A paid invoice answers 200 with "status": "paid", and a second payment is refused, so retrying is safe. USDC is offered on USD invoices whose business accepts it.
Webhooks
Add an endpoint in Settings → Developers and we'll POST a JSON event to it whenever something happens on your account — so your system finds out an invoice was paid without polling for it.
| Field | Type | Description |
|---|---|---|
| invoice.sent | event | An invoice was emailed to a customer. |
| invoice.paid | event | An invoice was marked paid. |
| quote.sent | event | A quote was emailed to a customer. |
| quote.converted | event | A quote was turned into an invoice. |
| receipt.created | event | A receipt was created through the API. |
The body wraps the same object the API returns, so you can reuse whatever parses your API responses:
{
"id": "evt_9c1f4a2b7e0d",
"type": "invoice.paid",
"created": 1790000000,
"data": { "object": "invoice", "id": "clx...", "total": 120, ... }
}Verifying the signature
Each request carries a Billery-Signature header — an HMAC-SHA256 of timestamp.bodyusing your endpoint's signing secret:
Billery-Signature: t=1790000000,v1=5f8d3c...
Billery-Event: invoice.paidCompute the same value over the raw request body and compare. Reject anything with a timestamp more than five minutes old, which stops a captured request being replayed later.
import crypto from "crypto";
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("="))
);
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(parts.v1, "hex")
);
}import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
if abs(time.time() - int(parts["t"])) > 300:
return False
expected = hmac.new(
secret.encode(),
f"{parts['t']}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Delivery behaviour
Reply with any 2xx status as soon as you've stored the event, and do the slow work afterwards — we give up on a request after 5 seconds. A non-2xx or a timeout is retried twice more with a short backoff; after that the event is dropped, and the failure is shown against the endpoint in your dashboard.
Treat handlers as idempotent: use the event idto ignore anything you've already processed, since a retry can arrive after your slow first attempt actually succeeded. Events aren't queued for later replay, so an endpoint that's down for a while will miss events — treat webhooks as a fast path, and the API as the source of truth.
Currently emitting: invoice.sent, invoice.paid, quote.sent, quote.converted, receipt.created.