Invoicing
Automate client billing, recurring invoices, expenses, payments, and credits in Anything Invoicing.
Anything Invoicing keeps a workspace's clients, invoices, recurring invoices, estimates, expenses, vendors, payments, and credits in one place. The API exposes the same records so an agent can import expenses, keep client and vendor lists in sync, draft and send invoices, run a billing schedule, and reconcile payments without opening the app.
The API is organization-scoped: every request reads and writes only the
organization that owns the key. The base URL is
https://invoicing.anything.solutions/api/v1 and all request
and response bodies are JSON.
Authentication and organizations
Create a key in the workspace at Settings → API (Settings → API keys). Only owners and admins can create or revoke keys. Give the key a name that identifies the agent or script, and choose whether it is read-only. The full token is shown once; the app keeps only its prefix, last four characters, and use history. Store it in the agent's secret store.
Send the token on every request:
Authorization: Bearer <token>
Keys belong to one organization. There is no org_id
parameter: the key determines the workspace, and records that belong to a
different organization are reported as 404. To automate two
organizations, switch to each one in the app and create a separate key.
- A read-only key can call
GETandHEADendpoints. Any write returns403with coderead_only. Use read-only keys for reporting and monitoring agents. - If the organization's billing is past due, writes return
402with codebilling_past_due. Reads continue to work. - Revoking a key takes effect immediately. Create a separate key for each agent so one can be revoked without disturbing the others.
- The API allows 120 requests per minute. Requests beyond that return
429with coderate_limited.
Request and response conventions
A successful request returns a data member. A single record
is an object, and a list adds pagination metadata:
{"data": {"id": 42, "name": "Example client"}}
{"data": [{"id": 42, "name": "Example client"}],
"meta": {"page": 1, "per_page": 25, "total": 1, "pages": 1}}
Send JSON bodies with Content-Type: application/json. A body
that is not a JSON object returns 415. Endpoints that need no
body, such as void and refund, can be called
without one.
| Value | Format |
|---|---|
| Money | Integer cents in *_cents fields, with the same amount as a decimal string in the matching field. Send amount or amount_cents; line items accept unit_price or unit_price_cents. Amounts must not be negative. |
| Dates | YYYY-MM-DD. Timestamps such as created_at are ISO 8601 strings. |
| Currency | Three-letter ISO code. New records default to the client's or organization's currency. |
| IDs | Integers. Every record belongs to exactly one organization. |
| Status | Responses include the stored status and an effective_status. overdue (invoices) and expired (estimates) are derived states, not stored values. Recurring schedules store active, paused, or finished. |
Errors
Errors use one shape, with an HTTP status that matches the condition:
{"error": {"code": "validation_error",
"message": "amount must be greater than zero."}}
| Status | Code | Meaning |
|---|---|---|
400 | bad_request | The request could not be parsed. |
401 | unauthorized | The key is missing, revoked, or not valid. |
402 | billing_past_due | The organization cannot write until billing is current. |
403 | read_only | The key is read-only and the request tried to write. |
404 | not_found | The record does not exist, or belongs to another organization. |
405 | method_not_allowed | That method is not available on the path. |
409 | conflict | The record is in a state that forbids the change, such as voiding a paid invoice or refunding a refunded payment. |
415 | invalid_content_type | The body is not a JSON object. |
422 | validation_error | A field is missing, malformed, or out of range. |
429 | rate_limited | Too many requests. Back off before trying again. |
500 | internal_error | The server failed. Read the record back before retrying a write. |
502 | email_failed | The invoice email could not be sent. The invoice's send state was not changed. |
Pagination and filters
List endpoints accept page (default 1) and
per_page (default 25, maximum
100). The response's meta repeats the page,
page size, total record count, and page count. Non-integer values return
422.
| Endpoint | Filters |
|---|---|
/clients | q, archived (1 or 0), pagination |
/vendors | q, status=active or status=archived, pagination |
/expense-categories | include_archived=1. Returns every built-in and custom category; no pagination. |
/expenses | q, category, vendor_id, client_id, from, to, pagination. Date bounds are inclusive. |
/invoices | q (number or client name), status, client_id, pagination |
/estimates | status, client_id, pagination |
/recurring-invoices | status, client_id, pagination |
/reports/summary | from, to (inclusive dates). Not paginated. |
/payments | invoice_id, client_id, status, pagination |
/credits | client_id, available=1, pagination |
/products | q, active (1 or 0), pagination |
/invoices?status= filters the stored status
(draft, sent, viewed,
partial, paid, void). To find
overdue invoices, use effective_status on the returned records
rather than the filter. Estimate statuses are draft,
sent, accepted, declined,
converted, and void; an expired estimate still
stores its original status.
Recurring schedules are active, paused, or
finished. /reports/summary defaults
to to today and from to 365 days before it; if
from is later than to, the two are swapped.
Endpoints
All paths are relative to
https://invoicing.anything.solutions/api/v1. {id}
is an integer record ID.
| Methods | Path | Purpose |
|---|---|---|
GET, PATCH | /organization | Read the organization profile, or update its name, currency, timezone, default terms and tax, footer, and default notes |
GET | /summary | Outstanding, overdue, month revenue and expenses, available credit, invoice and client counts |
GET | /reports/summary | Revenue, expenses, profit, outstanding, overdue, payment methods, and top clients for a date range |
GET, POST | /clients | List or create clients |
GET, PATCH | /clients/{id} | Get or update a client; archive with {"archived": true} |
GET | /clients/{id}/credits | Every credit for one client, without pagination |
GET, POST | /vendors | List or create vendors |
GET, PATCH, DELETE | /vendors/{id} | Get, update, or archive a vendor |
GET, POST | /expense-categories | List built-in and custom categories, or create a custom one |
GET, PATCH, DELETE | /expense-categories/{id} | Get, rename or reactivate, or archive a custom category |
GET, POST | /expenses | List or create expenses |
GET, PATCH, DELETE | /expenses/{id} | Get, update, or permanently delete an expense |
GET, POST | /invoices | List invoices or create a draft invoice |
GET, PATCH | /invoices/{id} | Get or update an invoice |
POST | /invoices/{id}/send | Email the invoice to its client and mark it sent |
POST | /invoices/{id}/mark-paid | Record the outstanding balance as paid |
POST | /invoices/{id}/duplicate | Create a new draft from an existing invoice |
POST | /invoices/{id}/void | Void an unpaid invoice and release applied credit |
POST | /invoices/{id}/apply-credit | Apply client credit, optionally limited with {"amount": "20.00"} |
GET, POST | /estimates | List estimates or create a draft estimate |
GET, PATCH | /estimates/{id} | Get or update an estimate that has not been converted |
POST | /estimates/{id}/convert | Create a draft invoice from an estimate and mark the estimate converted |
GET, POST | /recurring-invoices | List schedules or create a recurring invoice |
GET, PATCH, DELETE | /recurring-invoices/{id} | Get, update, or delete a schedule |
POST | /recurring-invoices/{id}/run | Generate one invoice immediately without advancing the schedule |
POST | /recurring-invoices/{id}/toggle | Switch between active and paused |
GET, POST | /payments | List or record payments |
GET | /payments/{id} | Get one payment |
POST | /payments/{id}/refund | Refund a completed payment in full |
GET, POST | /credits | List credits or create a manual client credit |
GET, POST | /products | List or create reusable line-item products |
GET, PATCH | /products/{id} | Get or update a product |
Clients, vendors, categories, and margin
Clients are the bill-to party on invoices and estimates. Vendors are the
businesses you pay. An expense can reference both: client_id
attributes the cost to a client, and vendor_id attributes it
to a payee. Send the IDs whenever you know them. If an expense has only a
vendor name, the API matches an existing vendor by name when
it can, but a name is not a reliable link.
Every expense also has a category. The built-in categories are returned
with "builtin": true and no id. Create a custom
category with POST /expense-categories, or simply use a new
name on an expense and the API creates it. Custom categories can be
renamed, archived with DELETE, and reactivated with
{"active": true}. Names are limited to 40 characters, are
compared without case, and cannot duplicate a built-in category.
Keep both links on imported costs. The reports page calculates client
margin as payments received from the client minus expenses attributed to
that client, and vendor spending from the vendor link. An expense with a
missing client_id is excluded from client margins, and one
with a missing vendor_id is excluded from vendor totals.
Invoice, estimate, and recurring actions
New invoices and estimates start as drafts. A draft can be edited with
PATCH; sending, paying, duplicating, voiding, and converting
are separate actions:
| Action | What it does |
|---|---|
POST /invoices/{id}/send | Emails the invoice and PDF link to the client's email address and marks it sent. A void or paid invoice returns 409. If the email cannot be delivered, the request returns 502 with code email_failed and the invoice keeps its previous send state. |
POST /invoices/{id}/mark-paid | Records the remaining balance as a completed payment with method other and reference Marked paid via API, then marks the invoice paid. Draft and void invoices return 409. A receipt may be emailed when notifications are enabled. If the balance is already zero, it does not create another payment. |
POST /invoices/{id}/duplicate | Creates a new draft with a new number, today's issue date, the organization's default terms, and copies of the lines, discount, notes, terms, and footer. The source invoice is unchanged. |
POST /estimates/{id}/convert | Creates a draft invoice from the estimate's client, lines, currency, notes, terms, and discount, then marks the estimate converted. Repeating the call returns the existing invoice with "converted": false and status 200. Void or declined estimates return 409. |
Recurring invoices are schedules that generate invoices. Create one with
client_id, name, and at least one line. The
optional fields are frequency (week,
month, quarter, year),
interval_count (1–24), start_date,
next_run_at (defaults to the start date), end_date,
auto_send (default true),
days_until_due (defaults to the organization's terms),
currency, notes, and terms. A new
schedule starts active.
POST /recurring-invoices/{id}/rungenerates one invoice immediately and returns it with status201. It does not advancenext_run_ator change the schedule's status. Whenauto_sendis true the generated invoice is markedsentand an email is attempted; the run still returns the invoice if that email fails. A schedule with no lines returns409. Because the schedule is not advanced, callingrunagain creates another invoice.POST /recurring-invoices/{id}/togglepauses an active schedule, or reactivates a paused or finished one. The call is not idempotent: a second toggle reverses the first.PATCH /recurring-invoices/{id}updates the schedule, includingstatus(active,paused,finished). Supplyinglinesreplaces the existing set.DELETE /recurring-invoices/{id}removes the schedule. It does not delete invoices the schedule already generated.
Organization settings and reports
PATCH /organization changes the organization name, currency,
timezone, default_terms_days (0–365), default_tax
(a percentage, or send default_tax_bps), footer,
and default_notes. The new currency and default terms apply to
records the API creates; the tax and document defaults are used by the
app's forms. Existing invoices, estimates, and schedules keep the values
they already store.
GET /reports/summary returns totals for an inclusive date
range. revenue is completed payments received in the range,
expenses is expenses incurred in the range, and
profit is the difference. outstanding and
overdue are current balances across all non-draft, non-void
invoices, not limited to the range. payment_methods breaks the
period's revenue down by method, and top_clients ranks up to
ten clients by revenue. The response also echoes from,
to, and the organization's currency.
The report sums every record in the range, so treat its totals as
authoritative only when the workspace records a single currency. If the
organization records payments or expenses in several currencies, group
those records by currency before drawing conclusions from the
combined total.
Examples
Set the key once and reuse it in the examples below:
export INVOICING_API_KEY="ivk_..."
List expenses for a month
curl
curl -sG "https://invoicing.anything.solutions/api/v1/expenses" \
-H "Authorization: Bearer $INVOICING_API_KEY" \
--data-urlencode "from=2026-10-01" \
--data-urlencode "to=2026-10-31" \
--data-urlencode "per_page=50"
Python
import os
import requests
API = "https://invoicing.anything.solutions/api/v1"
headers = {"Authorization": f"Bearer {os.environ['INVOICING_API_KEY']}"}
response = requests.get(
f"{API}/expenses",
headers=headers,
params={"from": "2026-10-01", "to": "2026-10-31", "per_page": 50},
timeout=30,
)
response.raise_for_status()
for expense in response.json()["data"]:
print(expense["id"], expense["incurred_on"], expense["vendor"], expense["amount"])
Create an expense with a vendor, client, and category
curl
curl -s -X POST "https://invoicing.anything.solutions/api/v1/expenses" \
-H "Authorization: Bearer $INVOICING_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"vendor_id": 12,
"client_id": 4,
"category": "Cloud hosting",
"description": "October hosting",
"amount": "240.00",
"currency": "USD",
"incurred_on": "2026-10-31",
"reference": "OCT-HOSTING"
}'
Python
payload = {
"vendor_id": 12,
"client_id": 4,
"category": "Cloud hosting",
"description": "October hosting",
"amount": "240.00",
"currency": "USD",
"incurred_on": "2026-10-31",
"reference": "OCT-HOSTING",
}
response = requests.post(
f"{API}/expenses",
headers={**headers, "Content-Type": "application/json"},
json=payload,
timeout=30,
)
response.raise_for_status()
expense = response.json()["data"]
print(expense["id"], expense["amount_cents"])
Create a draft invoice
curl
curl -s -X POST "https://invoicing.anything.solutions/api/v1/invoices" \
-H "Authorization: Bearer $INVOICING_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_id": 4,
"issue_date": "2026-11-03",
"due_date": "2026-12-03",
"currency": "USD",
"notes": "November retainer",
"lines": [
{"description": "Monthly retainer", "quantity": 1,
"unit_price": "1200.00", "tax": "0"}
]
}'
Python
payload = {
"client_id": 4,
"issue_date": "2026-11-03",
"due_date": "2026-12-03",
"currency": "USD",
"notes": "November retainer",
"lines": [
{"description": "Monthly retainer", "quantity": 1,
"unit_price": "1200.00", "tax": "0"},
],
}
response = requests.post(
f"{API}/invoices",
headers={**headers, "Content-Type": "application/json"},
json=payload,
timeout=30,
)
response.raise_for_status()
invoice = response.json()["data"]
print(invoice["id"], invoice["number"], invoice["status"], invoice["total"])
The response contains the server-assigned invoice number, totals, lines,
and "status": "draft". At least one line is required. For each
line, description is required; quantity defaults
to 1, unit_price to 0, and
tax and discount to 0 percent.
Passing lines on a later PATCH replaces the whole
set.
Create and run a recurring invoice
curl
curl -s -X POST "https://invoicing.anything.solutions/api/v1/recurring-invoices" \
-H "Authorization: Bearer $INVOICING_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_id": 4,
"name": "Monthly retainer",
"frequency": "month",
"interval_count": 1,
"start_date": "2026-11-01",
"next_run_at": "2026-11-01",
"auto_send": false,
"lines": [
{"description": "Monthly retainer", "quantity": 1,
"unit_price": "1200.00"}
]
}'
# Generate one invoice now, without advancing the schedule.
curl -s -X POST "https://invoicing.anything.solutions/api/v1/recurring-invoices/7/run" \
-H "Authorization: Bearer $INVOICING_API_KEY"
Python
schedule = {
"client_id": 4,
"name": "Monthly retainer",
"frequency": "month",
"interval_count": 1,
"start_date": "2026-11-01",
"next_run_at": "2026-11-01",
"auto_send": False,
"lines": [
{"description": "Monthly retainer", "quantity": 1,
"unit_price": "1200.00"},
],
}
created = requests.post(
f"{API}/recurring-invoices",
headers={**headers, "Content-Type": "application/json"},
json=schedule,
timeout=30,
)
created.raise_for_status()
schedule_id = created.json()["data"]["id"]
run = requests.post(
f"{API}/recurring-invoices/{schedule_id}/run",
headers=headers,
timeout=30,
)
run.raise_for_status()
invoice = run.json()["data"]
print(invoice["id"], invoice["number"], invoice["status"])
With auto_send false the generated invoice is a draft; with
it true the invoice is marked sent and an email is attempted.
Running the schedule again creates another invoice, so treat
run as a one-shot action and record the returned invoice ID.
Send, pay, duplicate, and convert
curl
# Email the invoice and mark it sent.
curl -s -X POST "https://invoicing.anything.solutions/api/v1/invoices/42/send" \
-H "Authorization: Bearer $INVOICING_API_KEY"
# Record the outstanding balance as paid.
curl -s -X POST "https://invoicing.anything.solutions/api/v1/invoices/42/mark-paid" \
-H "Authorization: Bearer $INVOICING_API_KEY"
# Copy the invoice into a new draft.
curl -s -X POST "https://invoicing.anything.solutions/api/v1/invoices/42/duplicate" \
-H "Authorization: Bearer $INVOICING_API_KEY"
# Turn an estimate into a draft invoice.
curl -s -X POST "https://invoicing.anything.solutions/api/v1/estimates/9/convert" \
-H "Authorization: Bearer $INVOICING_API_KEY"
Python
def post(path):
response = requests.post(f"{API}{path}", headers=headers, timeout=30)
response.raise_for_status()
return response.json()["data"]
sent = post("/invoices/42/send")
paid = post("/invoices/42/mark-paid")
copy = post("/invoices/42/duplicate")
invoice = post("/estimates/9/convert")
print(sent["status"], paid["status"], copy["status"], invoice["status"])
Each path is a separate action on a separate record; the IDs above are
examples. A repeat call to convert returns the same invoice
with "converted": false, but the other actions are not
replay-safe.
Read a period report
curl
curl -sG "https://invoicing.anything.solutions/api/v1/reports/summary" \
-H "Authorization: Bearer $INVOICING_API_KEY" \
--data-urlencode "from=2026-07-01" \
--data-urlencode "to=2026-09-30"
Python
response = requests.get(
f"{API}/reports/summary",
headers=headers,
params={"from": "2026-07-01", "to": "2026-09-30"},
timeout=30,
)
response.raise_for_status()
report = response.json()["data"]
print(report["from"], report["to"], report["revenue"], report["profit"])
Safe automation
The API does not support an Idempotency-Key header and does
not deduplicate repeated writes. A retried POST can create a
second invoice, recurring invoice, expense, payment, or credit. The safe
pattern is read, then retry:
- Give each write a key you control:
referencefor expenses, payments, and credits; a distinctivenotesvalue or line description for invoices and recurring schedules; the exact name for vendors and categories. - After a timeout,
500, or dropped connection,GETthe collection and look for that key before sending the request again. For invoices, list the client's invoices and compare the date, total, notes, and lines. For payments, list byinvoice_idorclient_idand compare amount and reference. For a recurring run, list the client's invoices and compare the recent draft or sent invoice with the schedule's line description. - Only when the record is absent, retry the write. Once it exists, keep
its
idand continue from there. - After every successful create,
GETthe record byidand store the ID in your own system. Do not rely on a name match to identify the new record later.
Some actions are not replay-safe. duplicate,
recurring-invoices/{id}/run, and
recurring-invoices/{id}/toggle change state on every call:
a retry creates another draft, another invoice, or flips the schedule back.
Never retry them automatically. send can email the client
twice, so if its response is lost, GET the invoice and check
its status before resending. mark-paid is safe to repeat once
the invoice is paid, and estimates/{id}/convert returns the
existing invoice on a repeat call.
Retry reads on 429 and 5xx with exponential
backoff. Do not retry writes automatically: a write that returns an error
may still have committed. Treat 409 as a state signal rather
than a transient failure, fetch the current record, and reconcile. A
402 or 403 will not succeed until billing or key
permissions change, so surface it instead of looping.
- Use a read-only key for reporting and reconciliation jobs. Use a separate read and write key only for the agent that must create or change records.
- Keep one key per organization. An ID from another organization returns
404, not a cross-organization record. - Do not mix currencies in a total. Each record carries its own
currency; group by it before summing. - Delete is permanent for expenses and recurring schedules. Vendors, categories, and clients are archived instead, and invoices are voided rather than deleted. Refunds reverse a completed payment in full; there is no partial-refund amount.