Expenses
Record the money your business spends and track it against customers and projects.
Overview
An expense is a single record of money spent: a software subscription, a flight, a lunch with a client, a payment processor fee. Unlike invoices, expenses have no line items: each one has an amount, a currency, an optional tax, a vendor, a category, and optional links to a customer or project so you can measure margin.
Each expense gets an auto-generated expense_number (e.g. EXP-000001). The prefix is configurable per company in Settings.
Expenses are an optional module. If an owner or admin turned it off in Settings > Modules, every expenses, vendors, and expense categories endpoint returns 403 with code expenses_module_disabled.
Endpoints
| Method | Path | Permission | Description |
|---|---|---|---|
GET | /v1/expenses | read | List expenses (cursor pagination, filters) |
POST | /v1/expenses | write | Create an expense |
GET | /v1/expenses/{id} | read | Retrieve an expense by id or expense_number |
POST | /v1/expenses/{id} | write | Update an expense (PATCH is accepted as an alias) |
DELETE | /v1/expenses/{id} | admin | Delete an expense (soft delete) |
Create an Expense
expense_date, amount, and currency are required. The response is the created Expense object with status 201. Expenses created through the API are recorded with source: "api".
You can pass vendor_name and category_name instead of ids. Corebill looks them up by name (case-insensitive) and creates them if they don't exist yet. If you send both, the name wins over vendor_id / category_id.
Amounts and tax
amount is the value you paid in the major unit of the currency with 2 decimals (120.5 = $120.50). By default it is tax inclusive: Corebill splits it into subtotal + tax_amount = total. Send tax_inclusive: false if amount is before tax; then amount becomes the subtotal and the tax is added on top.
Pass tax_id to use one of the company's configured taxes, or tax_rate (0-100) for a one-off percentage. tax_id takes precedence over tax_rate.
Recurring expenses
Set recurrence to weekly, monthly, or yearly for costs that repeat (subscriptions, rent). The expense you create is the first occurrence; Corebill creates the next copies automatically on recurrence_next_date until the optional recurrence_end_date. Generated copies have source: "recurring" and point to the original in recurring_parent_id.
List Expenses
Returns expenses most recently created first, using cursor pagination (limit, starting_after, ending_before).
| Query param | Type | Description |
|---|---|---|
from | date | Only expenses with expense_date on or after this date (YYYY-MM-DD) |
to | date | Only expenses with expense_date on or before this date (YYYY-MM-DD) |
customer_id | string | Filter by linked customer (cus_...) |
project_id | string | Filter by linked project |
category_id | string | Filter by category (exc_...) |
vendor_id | string | Filter by vendor (ven_...) |
limit | integer | 1-100. Default: 10 |
starting_after / ending_before | string | Cursor (an expense id) |
Retrieve an Expense
Accepts the expense id (exp_...) or its expense_number.
Update an Expense
Send only the fields you want to change. Amounts are recalculated when you change amount, tax_id, tax_rate, or tax_inclusive. Send null to clear an optional link (e.g. "project_id": null). Use POST like the rest of the API; PATCH is accepted as an alias.
Delete an Expense
Deleting an expense is a soft delete (sets deleted_at). If the expense was recurring, it stops generating new copies. Requires admin permission.
The Expense Object
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier (prefix: exp_) |
expense_number | string | Human-readable number (e.g. EXP-000042) |
expense_date | date | Date the money was spent (YYYY-MM-DD) |
subtotal / tax_amount / total | number | Amounts in major unit, 2 decimals. total = subtotal + tax_amount |
tax_rate | number | Tax percentage applied (0-100) |
tax_inclusive | boolean | Whether the amount you sent already included tax |
vendor / category | object | { id, name } of the linked vendor / category, or null |
receipt | object | Metadata of the attached receipt (name, content_type, size), or null. Receipts are uploaded from the dashboard |
source | string | manual, api, agent, recurring, or stripe (processor fees imported automatically) |
recurrence | string | none, weekly, monthly, or yearly |
recurrence_next_date | date | When the next copy will be created, or null |
recurring_parent_id | string | For generated copies, the id of the original expense |
Expense Fields
Fields accepted by create and update. Any other field in the body is ignored.
| Field | Type | Required | Description |
|---|---|---|---|
expense_date | string | Yes (create) | YYYY-MM-DD |
amount | number | Yes (create) | Greater than 0, major unit with 2 decimals. Tax included unless tax_inclusive is false |
currency | string | Yes (create) | ISO-4217 code (e.g. USD, EUR, PEN) |
tax_inclusive | boolean | No | Default: true |
tax_id | string | No | One of the company's configured taxes. Takes precedence over tax_rate |
tax_rate | number | No | Tax percentage 0-100 when no tax_id is sent |
vendor_id | string | No | Existing vendor (ven_...) |
vendor_name | string | No | Vendor name. Found or created automatically |
category_id | string | No | Existing category (exc_...) |
category_name | string | No | Category name. Found or created automatically |
description | string | No | What the expense was for (max 500 chars) |
payment_method | string | No | bank_transfer, credit_card, debit_card, cash, check, paypal, stripe, wallet, other |
customer_id | string | No | Customer this expense is for (cus_...) |
project_id | string | No | Project this expense is for |
notes | string | No | Internal notes |
recurrence | string | No | none (default), weekly, monthly, yearly |
recurrence_end_date | string | No | YYYY-MM-DD. Last date a copy can be created |
All referenced ids (vendor, category, tax, customer, project) must belong to the same company as your API key. Otherwise the request fails with 400 and code resource_missing.
Errors
| Status | Code | When |
|---|---|---|
400 | parameter_missing | expense_date, amount, or currency missing on create, or an empty update |
400 | invalid_expense_date, invalid_amount, invalid_currency, invalid_tax_rate, invalid_payment_method, invalid_recurrence, invalid_recurrence_end_date | A field has an invalid value (param names the field) |
400 | resource_missing | A referenced vendor, category, tax, customer, or project does not exist in this company |
400 | invalid_date | from or to query param is not YYYY-MM-DD |
403 | permission_required | The API key lacks write (create/update) or admin (delete) permission |
403 | expenses_module_disabled | The Expenses module is turned off for the company |
404 | expense_not_found | No expense with that id or expense_number in this company |
Expenses emit expense.created, expense.updated and expense.deleted webhook events (including expenses created automatically from Stripe fees and recurring expenses). See Webhooks.