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
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

  1. In Claude, open Customize → Connectors, click + and choose Add custom connector.
  2. Name it Billery, enter https://app.billery.io/mcp as the URL and click Add. Leave the advanced settings empty.
  3. Click Connect, sign in to Billery and approve access. You're sent back to Claude.
  4. 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:

shell
claude mcp add --transport http billery https://app.billery.io/mcp

What 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:

FieldTypeDescription
May createruleWhether it can add customers, invoices, quotes, receipts and usage.
SendingruleAutomatic, only with your approval, or never.
Automatic limitruleSends above this amount wait for approval, e.g. anything over $500.
Daily limitruleSends beyond this many a day wait for approval.
ExpiresruleThe 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:

json
{
  "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: 1790000040

X-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.

curl
-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:

json
{
  "error": {
    "type": "invalid_request",
    "message": "At least one item is required",
    "param": "items"
  }
}
FieldTypeDescription
authentication_error401Missing, invalid or revoked key.
invalid_request400Malformed JSON or a field that failed validation. Check param.
permission_denied403An agent key tried something its rules don't allow. The message says why.
not_found404No such document or customer on your account.
limit_reached402Free document allowance used up — upgrade to Pro.
conflict409A request with this Idempotency-Key is still processing.
rate_limited429Too many requests. Wait for Retry-After.
server_error500Something broke on our side. Safe to retry with the same idempotency key.

Invoices

post/api/v1/invoices

Creates an invoice. Pass send: true to email the PDF to the customer immediately; otherwise it lands in your account as a draft.

FieldTypeDescription
customer_idstringId of an existing customer. Use this or customer, not both.
customerobjectNew or matched customer: name and email required; phone, address, city, country optional. Matched on email.
itemsarrayOne to 100 line items, each with description, quantity and unit_price.
currencystringThree-letter code. Defaults to the customer's currency, then GBP.
issue_datestringYYYY-MM-DD. Defaults to today.
due_datestringYYYY-MM-DD. Must not be before issue_date.
due_in_daysintegerDays from the issue date. Defaults to 14. Ignored if due_date is set.
payment_methodsarrayHow the customer may pay: any of bank, card, usdc. Defaults to every method you've set up for the currency.
tax_ratenumberPercentage from 0 to 100. Applied after the discount.
discountnumberFlat amount off the subtotal. Cannot exceed it.
notesstringFree text printed on the document.
sendbooleanEmail the document to the customer as a PDF straight away. With an agent key this follows the agent's sending rules.
json
{
  "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).

get/api/v1/invoices/{id}

Retrieves an invoice, including its current status and payment date.

Quotes

post/api/v1/quotes

Same shape as an invoice, with an expiry instead of a due date. Accepted quotes can be converted to an invoice from your dashboard.

FieldTypeDescription
expiry_datestringYYYY-MM-DD. When the quote stops being valid.
valid_for_daysintegerDays 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.

get/api/v1/quotes/{id}

Receipts

post/api/v1/receipts

A 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.

FieldTypeDescription
paid_atstringYYYY-MM-DD. When payment was taken. Defaults to today.
payment_methodstringPrinted 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.

post/api/v1/usage
FieldTypeDescription
customer_idstringThe customer who used it.
descriptionstringAs it should read on the invoice, e.g. "Support tickets resolved".
quantitynumberHow many units.
unit_pricenumberPrice per unit.
currencystringDefaults to the customer's currency.
occurred_atstringISO 8601 time it happened. Defaults to now.
idstringYour own id for the event. Sending the same id again returns the first event (200) instead of recording it twice.
metadataobjectAnything you want to keep with the event.
curl
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"
  }'
get/api/v1/usage

Lists events, newest first. Filter with customer_id and status (unbilled, billed or all); page with limit (up to 100) and offset.

post/api/v1/usage/invoice

Rolls 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
curl https://app.billery.io/i/8f2c... -H "Accept: application/json"
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:

  1. Request the url. It answers 402 Payment Required with the requirements in the PAYMENT-REQUIRED header (and the body).
  2. Sign an EIP-3009 USDC transfer authorisation for exactly amount to payTo, the business's own wallet.
  3. Repeat the request with it in the PAYMENT-SIGNATURE header. The payment is settled on chain, the invoice is marked paid, and you get 200 with the transaction and a PAYMENT-RESPONSE header.
json
{
  "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.

FieldTypeDescription
invoice.senteventAn invoice was emailed to a customer.
invoice.paideventAn invoice was marked paid.
quote.senteventA quote was emailed to a customer.
quote.convertedeventA quote was turned into an invoice.
receipt.createdeventA receipt was created through the API.

The body wraps the same object the API returns, so you can reuse whatever parses your API responses:

json
{
  "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.paid

Compute 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.

node.js
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")
  );
}
python
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.