Expenses

Record and read business expenses over the API. The read shape is a field-by-field allowlist over the stored record, so receipts, OCR output, draft flags, recurrence links, and audit metadata are never returned.

Expense endpoints require a secret key (ibp_sk_). expenses:read and expenses:write are outside the publishable-key allowlist, so a publishable key can never hold them.

The expense model

Properties

  • Name
    id
    Type
    string
    Description

    Unique identifier. Server-generated for expenses created through the API.

  • Name
    amount
    Type
    number
    Description

    Expense amount, rounded to 2 decimal places.

  • Name
    currency
    Type
    string
    Description

    ISO 4217 code, uppercase.

  • Name
    category / subcategory
    Type
    string
    Description

    Canonical category id (see expense categories) and a free-text subcategory. subcategory is full shape only.

  • Name
    date
    Type
    string
    Description

    Expense date, returned as an ISO 8601 timestamp. Stored internally as epoch seconds, so a date with no time component reads back at 00:00:00.000Z.

  • Name
    description / notes
    Type
    string
    Description

    Free text. notes is full shape only.

  • Name
    tags
    Type
    array
    Description

    Free-form tags, [] when none. Full shape only.

  • Name
    supplier
    Type
    object
    Description

    { id, name }, either of which may be null. null when no supplier is set. Full shape only.

  • Name
    payment
    Type
    object
    Description

    method, status, due_date, reference. Always present, with null members when unset. Full shape only.

  • Name
    tax
    Type
    object
    Description

    { amount, rate }, or null when no tax block is stored. Full shape only.

  • Name
    status
    Type
    string
    Description

    Lifecycle status. Expenses created through the API are always submitted. Dashboard expenses awaiting approval are draft; automatic gateway-fee expenses are recorded.

  • Name
    source
    Type
    string
    Description

    Provenance. api for expenses created through this endpoint, gateway_fee for the automatic payment-fee entries, other values for dashboard and recurring expenses.

  • Name
    editable
    Type
    boolean
    Description

    Whether the expense can be edited in the dashboard. Automatic gateway-fee expenses are false.

  • Name
    line_items / metadata
    Type
    array / object
    Description

    Gateway-fee expenses only, full shape only. See Gateway-fee expenses.

  • Name
    created_at / updated_at
    Type
    string
    Description

    ISO 8601 timestamps. created_at is full shape only.

The list endpoint returns a lighter shape: id, amount, currency, category, date, status, source, editable, supplier_name, description, and updated_at. Note that the supplier arrives as a flat supplier_name string, not the supplier object. Retrieve an expense by id for the full shape.


GET/api/v2/expenses

List all expenses

Retrieve a cursor-paginated list of expenses. Soft-deleted expenses are excluded; gateway-fee expenses are included.

Required scope: expenses:read

Query parameters

  • Name
    limit
    Type
    integer
    Description

    Page size (default 50, max 100). A non-numeric value falls back to 50.

  • Name
    cursor
    Type
    string
    Description

    Opaque cursor from pagination.next_cursor.

  • Name
    category
    Type
    string
    Description

    Exact match on the canonical category id. Labels are not resolved here, unlike on create.

  • Name
    status
    Type
    string
    Description

    Exact match on the lifecycle status.

  • Name
    supplier_id
    Type
    string
    Description

    Exact match on the supplier id.

  • Name
    date_from / date_to
    Type
    string
    Description

    Inclusive range on the expense date. Accepts epoch seconds or ISO 8601. An unparseable value returns 400 naming the field.

  • Name
    updated_after
    Type
    string
    Description

    ISO 8601. Returns expenses whose updated_at is greater than or equal to this value, ordered by updated_at descending. An unparseable value returns 400.

  • Name
    order_by
    Type
    string
    Description

    date (default, descending) or updated_at (descending). Any other value returns 400.

Allowed filter combinations

A date range can only be applied when the results are ordered by date, so the parameters that force the updated_at order are rejected rather than silently dropped. Exactly one of these three shapes is valid:

  • date_from and/or date_to, ordered by date. This is the default order, so order_by may be omitted or set to date.
  • updated_after, with no date range. Results come back ordered by updated_at descending.
  • order_by=updated_at, with no date range.

Each shape composes with at most one of category, status, or supplier_id.

These return 400:

  • updated_after together with date_from or date_to, code conflicting_filters.
  • order_by=updated_at together with date_from or date_to, code conflicting_filters.
  • Two or more of category, status, and supplier_id, code invalid_value, with field naming the second offender.
  • Any order_by value other than date or updated_at, code invalid_value.

Pagination

Pass pagination.next_cursor back as cursor. A cursor that is malformed, or that points at a document that no longer exists, returns 400 with code invalid_cursor. Treat that as an error and restart the list without a cursor; it is not the end of the list.

Soft-deleted expenses are removed after the page window is read, so a page can contain fewer than limit items. Use has_more and next_cursor to decide whether to continue, not the item count.

For delta sync, pass the highest updated_at you have seen as updated_after. The comparison is inclusive, so the last record of the previous sync is returned again.

Request

GET
/api/v2/expenses
curl -G https://au.api.ibakepro.com/api/v2/expenses \
  -H "Authorization: Bearer {secret_key}" \
  -d category=ingredients \
  -d date_from=2026-06-01 \
  -d limit=10

Response

{
  "data": [
    {
      "id": "Yb4Nq7TdWm2Fs9Ke1Rzu",
      "amount": 124.50,
      "currency": "AUD",
      "category": "ingredients",
      "date": "2026-06-01T00:00:00.000Z",
      "status": "submitted",
      "source": "api",
      "editable": true,
      "supplier_name": "Southbank Flour Co",
      "description": "Bulk flour",
      "updated_at": "2026-06-01T10:30:00.000Z"
    }
  ],
  "pagination": {
    "limit": 10,
    "has_more": false,
    "next_cursor": null,
    "prev_cursor": null
  }
}

Response (400)

{
  "error": {
    "type": "validation_error",
    "code": "conflicting_filters",
    "message": "updated_after cannot be combined with date_from. Use updated_after for delta sync, or date_from for a date range.",
    "field": "date_from"
  }
}

POST/api/v2/expenses

Create an expense

Create an expense.

Required scope: expenses:write

Required attributes

  • Name
    amount
    Type
    number
    Description

    Must be a finite number greater than 0. Zero, a negative amount, and a non-numeric value all return 400. Stored rounded to 2 decimal places.

  • Name
    date
    Type
    string
    Description

    ISO 8601, or a string of epoch seconds. Must resolve to a positive epoch value, so dates at or before 1970-01-01T00:00:00Z are rejected.

  • Name
    category
    Type
    string
    Description

    A category id or its label, matched case-insensitively (see expense categories). The server stores the canonical id. An unknown value returns 400.

Optional attributes

  • Name
    currency
    Type
    string
    Description

    Three letters, uppercased before validation. Anything else returns 400. Omit to use the account currency; if that cannot be resolved the request returns 500.

  • Name
    subcategory / description / notes
    Type
    string
    Description

    Free text.

  • Name
    tags
    Type
    array
    Description

    Free-form tags. A non-array value is stored as [].

  • Name
    supplier
    Type
    object
    Description

    { id, name }. Any other key is discarded.

  • Name
    payment
    Type
    object
    Description

    { method, status, due_date, reference }. Only these four keys are accepted.

  • Name
    tax
    Type
    object
    Description

    { amount, rate }. amount is rounded to 2 decimal places. Only these two keys are accepted.

All string values are trimmed and stripped of HTML before storage, except description and notes, which keep their markup.

Server-controlled fields

The request body cannot set provenance or lifecycle. These are forced on every API create and any value you send for them is discarded:

  • source is api. A gateway-fee expense cannot be forged through this endpoint.
  • status is submitted and the draft flag is off. The dashboard approval gate does not apply to API creates.
  • editable is true, auto_generated is false, and the id is server-generated.
  • line_items and metadata belong to gateway-fee expenses and are never accepted on create.

Response

201 with the full expense shape.

Idempotency

Send an Idempotency-Key header. A replay of a stored key returns the original response with an Idempotent-Replayed: true header. Reusing a key with a different body, or on a different route, returns 422 (idempotency_key_reused), and a retry while the first request is still running returns 409 (request_in_flight). Keys expire 24 hours after first use.

Request

POST
/api/v2/expenses
curl -X POST https://au.api.ibakepro.com/api/v2/expenses \
  -H "Authorization: Bearer {secret_key}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: exp-0001" \
  -d '{
    "amount": 124.50,
    "date": "2026-06-01T00:00:00Z",
    "category": "ingredients",
    "description": "Bulk flour",
    "supplier": { "name": "Southbank Flour Co" },
    "tax": { "amount": 11.32, "rate": 0.1 }
  }'

Response (201)

{
  "id": "Yb4Nq7TdWm2Fs9Ke1Rzu",
  "amount": 124.50,
  "currency": "AUD",
  "category": "ingredients",
  "subcategory": null,
  "description": "Bulk flour",
  "notes": null,
  "tags": [],
  "date": "2026-06-01T00:00:00.000Z",
  "status": "submitted",
  "source": "api",
  "editable": true,
  "supplier": { "id": null, "name": "Southbank Flour Co" },
  "payment": { "method": null, "status": null, "due_date": null, "reference": null },
  "tax": { "amount": 11.32, "rate": 0.1 },
  "created_at": "2026-06-01T10:30:00.000Z",
  "updated_at": "2026-06-01T10:30:00.000Z"
}

GET/api/v2/expenses/:id

Retrieve an expense

Retrieve a single expense by id, in the full shape. Returns 404 if the expense does not exist, is soft-deleted, or belongs to another account.

Required scope: expenses:read

Request

GET
/api/v2/expenses/Yb4Nq7TdWm2Fs9Ke1Rzu
curl https://au.api.ibakepro.com/api/v2/expenses/Yb4Nq7TdWm2Fs9Ke1Rzu \
  -H "Authorization: Bearer {secret_key}"

Gateway-fee expenses

When a payment gateway deducts a processing fee, the account records it automatically as one expense per day per account. They appear in list and retrieve responses so your expense total reconciles with the dashboard, and they cannot be created, edited, or deleted over the API.

They are recognisable by source: "gateway_fee" and editable: false. Their id is gateway_fees_ followed by the date (gateway_fees_2026-06-01), the category is bank_fees, the status is recorded, the supplier name is Payment gateways, and payment.status is deducted_at_source.

Retrieving one by id adds two fields that no other expense carries:

  • Name
    line_items
    Type
    array
    Description

    One entry per fee-bearing transaction: order_id, order_label, gateway, amount.

  • Name
    metadata
    Type
    object
    Description

    transaction_count and gateways, the distinct gateway names in the breakdown.

Neither field is present in the list shape, so read the expense by id when you need the per-transaction breakdown.