anything.solutions

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 GET and HEAD endpoints. Any write returns 403 with code read_only. Use read-only keys for reporting and monitoring agents.
  • If the organization's billing is past due, writes return 402 with code billing_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 429 with code rate_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.

ValueFormat
MoneyInteger 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.
DatesYYYY-MM-DD. Timestamps such as created_at are ISO 8601 strings.
CurrencyThree-letter ISO code. New records default to the client's or organization's currency.
IDsIntegers. Every record belongs to exactly one organization.
StatusResponses 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."}}
StatusCodeMeaning
400bad_requestThe request could not be parsed.
401unauthorizedThe key is missing, revoked, or not valid.
402billing_past_dueThe organization cannot write until billing is current.
403read_onlyThe key is read-only and the request tried to write.
404not_foundThe record does not exist, or belongs to another organization.
405method_not_allowedThat method is not available on the path.
409conflictThe record is in a state that forbids the change, such as voiding a paid invoice or refunding a refunded payment.
415invalid_content_typeThe body is not a JSON object.
422validation_errorA field is missing, malformed, or out of range.
429rate_limitedToo many requests. Back off before trying again.
500internal_errorThe server failed. Read the record back before retrying a write.
502email_failedThe 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.

EndpointFilters
/clientsq, archived (1 or 0), pagination
/vendorsq, status=active or status=archived, pagination
/expense-categoriesinclude_archived=1. Returns every built-in and custom category; no pagination.
/expensesq, category, vendor_id, client_id, from, to, pagination. Date bounds are inclusive.
/invoicesq (number or client name), status, client_id, pagination
/estimatesstatus, client_id, pagination
/recurring-invoicesstatus, client_id, pagination
/reports/summaryfrom, to (inclusive dates). Not paginated.
/paymentsinvoice_id, client_id, status, pagination
/creditsclient_id, available=1, pagination
/productsq, 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.

MethodsPathPurpose
GET, PATCH/organizationRead the organization profile, or update its name, currency, timezone, default terms and tax, footer, and default notes
GET/summaryOutstanding, overdue, month revenue and expenses, available credit, invoice and client counts
GET/reports/summaryRevenue, expenses, profit, outstanding, overdue, payment methods, and top clients for a date range
GET, POST/clientsList or create clients
GET, PATCH/clients/{id}Get or update a client; archive with {"archived": true}
GET/clients/{id}/creditsEvery credit for one client, without pagination
GET, POST/vendorsList or create vendors
GET, PATCH, DELETE/vendors/{id}Get, update, or archive a vendor
GET, POST/expense-categoriesList 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/expensesList or create expenses
GET, PATCH, DELETE/expenses/{id}Get, update, or permanently delete an expense
GET, POST/invoicesList invoices or create a draft invoice
GET, PATCH/invoices/{id}Get or update an invoice
POST/invoices/{id}/sendEmail the invoice to its client and mark it sent
POST/invoices/{id}/mark-paidRecord the outstanding balance as paid
POST/invoices/{id}/duplicateCreate a new draft from an existing invoice
POST/invoices/{id}/voidVoid an unpaid invoice and release applied credit
POST/invoices/{id}/apply-creditApply client credit, optionally limited with {"amount": "20.00"}
GET, POST/estimatesList estimates or create a draft estimate
GET, PATCH/estimates/{id}Get or update an estimate that has not been converted
POST/estimates/{id}/convertCreate a draft invoice from an estimate and mark the estimate converted
GET, POST/recurring-invoicesList schedules or create a recurring invoice
GET, PATCH, DELETE/recurring-invoices/{id}Get, update, or delete a schedule
POST/recurring-invoices/{id}/runGenerate one invoice immediately without advancing the schedule
POST/recurring-invoices/{id}/toggleSwitch between active and paused
GET, POST/paymentsList or record payments
GET/payments/{id}Get one payment
POST/payments/{id}/refundRefund a completed payment in full
GET, POST/creditsList credits or create a manual client credit
GET, POST/productsList 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:

ActionWhat it does
POST /invoices/{id}/sendEmails 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-paidRecords 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}/duplicateCreates 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}/convertCreates 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}/run generates one invoice immediately and returns it with status 201. It does not advance next_run_at or change the schedule's status. When auto_send is true the generated invoice is marked sent and an email is attempted; the run still returns the invoice if that email fails. A schedule with no lines returns 409. Because the schedule is not advanced, calling run again creates another invoice.
  • POST /recurring-invoices/{id}/toggle pauses 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, including status (active, paused, finished). Supplying lines replaces 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:

  1. Give each write a key you control: reference for expenses, payments, and credits; a distinctive notes value or line description for invoices and recurring schedules; the exact name for vendors and categories.
  2. After a timeout, 500, or dropped connection, GET the 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 by invoice_id or client_id and 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.
  3. Only when the record is absent, retry the write. Once it exists, keep its id and continue from there.
  4. After every successful create, GET the record by id and 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.