Resources

Expenses

An expense is a purchase or a mileage entry, optionally linked to a project, optionally carrying a photographed receipt. Expenses are the job-cost side of the workspace: the endpoints here exist so an outside system can react to a purchase the moment it is captured in the field, read everything about it including the receipt image, and record back that it has been handled.

Amounts include tax

amount_tax_inclusive is the grand total actually paid, with sales tax already in it. It is not a subtotal. If your system applies its own tax, apply it to zero, or you will inflate the record by the tax rate. When the receipt printed a separate tax line,tax_amount_included tells you how much of the total that was, so a $35.70 expense with $2.72 of tax had a $32.98 subtotal.

Permissions

Expenses are financial records, so an API key does not read them just because it is a valid key. Every endpoint here checks the permissions of the workspace member who generated the key, exactly as the app would for that member, and every endpoint requires the workspace to be on the Professional plan.

EndpointTypeDescription
GET /v1/expensespermissionexpenses.view and expenses.view_all. A key whose owner can only see their own expenses is refused rather than served a silently partial workspace.
GET /v1/expenses/{id}permissionexpenses.view, plus either expenses.view_all or authorship of that expense.
GET /v1/expenses/{id}/receiptpermissionSame as retrieving the expense.
PUT / DELETE external-referencespermissionexpenses.edit_all, or expenses.edit on an expense the key owner created.
GET/v1/expenses

List expenses

Returns expenses newest first by expense_date, with id as a tiebreak so page boundaries stay stable between requests. See pagination and rate limits for how to page a large result set safely.

Query parameters

NameTypeDescription
project_iduuidOnly expenses linked to this project.
date_fromYYYY-MM-DDOnly expenses on or after this date.
date_toYYYY-MM-DDOnly expenses on or before this date.
reconciledbooleantrue returns expenses that at least one system has stamped; false returns expenses nothing has handled yet.
reconciled_bystringOnly expenses carrying an external reference in this namespace.
unreconciled_bystringOnly expenses with no external reference in this namespace. This is the sweep query: "what have I not handled yet".
expense_typeenumpurchase or mileage.
has_receiptbooleanOnly expenses that do (or do not) have a receipt attached.
include_archivedbooleanSet true to include archived expenses. Defaults to false.
limitintegerRows per page, 1 to 100. Defaults to 25.
offsetintegerRows to skip. Defaults to 0.

Returns

An object with data (an array of expense objects), has_more, limit, and offset. has_more is true when the page came back full; it is not an exact remaining count, so keep paging until it is false.

GET/v1/expenses/{id}

Retrieve an expense

Returns a single expense object. An expense your key may not read returns 404 not_found rather than 403, so a key cannot use this endpoint to prove a record exists.

The expense object

FieldTypeDescription
idstringUnique id of the expense.
objectstringAlways expense.
expense_typeenumpurchase or mileage.
expense_datestringDate of the purchase, YYYY-MM-DD.
amount_tax_inclusivenumberThe total actually paid, INCLUDING sales tax. This is not a pre-tax subtotal. Do not apply tax on top of this figure.
tax_amount_includednumber | nullThe sales tax portion contained within amount_tax_inclusive, when the receipt broke it out separately. Never an addition to the amount. Null when unknown.
currencystringAlways USD. Coastline is single-currency today.
vendorobject | nullThe merchant, as { id, name }. id is null when the expense carries only a free-text merchant name.
categoryobject | nullThe expense category, as { id, name }.
descriptionstring | nullWhat was purchased.
notesstring | nullFree-form internal note on the expense.
reference_numberstring | nullReceipt or transaction number printed on the document.
payment_methodenumcompany_card, company_check, company_cash, personal_funds, or mileage.
reimbursement_statusenumnone, requested, approved, paid, or rejected.
billablebooleanWhether the expense is marked billable to the customer.
is_archivedbooleanArchived expenses are excluded from list responses unless include_archived=true.
mileageobject | nullFor mileage expenses, { miles_driven, rate }. Null for purchases.
projectobject | nullThe linked project, including its address and its resolved custom fields. Null when the expense is not project-linked.
receiptobject | nullThe captured receipt, as { url, expires_at, mime_type, size_bytes }. See Receipts below.
external_referencesobjectNamespaced record of which outside systems have already handled this expense. See Reconciliation below.
created_bystringId of the workspace member who captured the expense.
created_atstringISO 8601 timestamp.
updated_atstringISO 8601 timestamp.

Receipts

A receipt often carries information the structured record does not: the store location, the time of purchase, and the subtotal and tax broken out separately. When an expense has one, the receipt object on the payload carries a signed URL valid for 24 hours that needs no authentication to fetch. That URL is minted fresh every time the expense is serialized, including on each webhook delivery attempt, so a retried delivery never carries an expired link.

Treat the signed URL as a credential: anyone holding it can fetch the image until it expires. Do not log it or forward it outside your integration.

If you would rather hold a stable URL and resolve it on demand, GET /v1/expenses/{id}/receipt authenticates with your API key and responds 307 to a freshly signed URL. It returns 404 not_found when the expense has no receipt.

Projects and custom fields

A project-linked expense embeds the project on the payload, including its address and its resolved custom fields. Custom fields are how a workspace stores its own data on a project, and they are usually the only reliable way to match a Coastline project to a record in another system, since customer names rarely agree across systems. Fields are addressed by key, which is stable, rather than by label, which a workspace admin can rename at any time. See the custom fields reference.

PUT/v1/expenses/{id}/external-references/{namespace}

Reconciliation write-back

Webhook delivery is at-least-once, jobs get retried, and nightly sweeps overlap. Without a durable record on the expense itself that it has already been handled, a duplicate delivery creates a duplicate record in your system. Stamp the expense as soon as you have an identifier for it, then filter it out on the next pass with ?unreconciled_by={namespace}.

The namespace is yours to choose: 1 to 64 lowercase letters, digits, hyphens or underscores. It keeps integrations from colliding, so an accounting sync and an ordering sync can both stamp the same expense and neither overwrites the other.

The write is idempotent. Repeating an identical stamp changes nothing at all, not even updated_at. Changing the identifier updates it in place and preserves the original created_at. A write-back never fires an expense.updated webhook, so stamping an expense in response to a delivery cannot feed that delivery back to you.

Body parameters

NameTypeDescription
external_idstringRequired. The identifier the outside system assigned, 255 characters or fewer.
urlstringAbsolute http or https link to the record in the outside system.
metadataobjectArbitrary JSON your integration wants to keep alongside the reference. 4 KB or less when serialized.
DELETE/v1/expenses/{id}/external-references/{namespace}

Clear a reference

Removes one namespace and leaves the others untouched, so the expense returns to the unhandled pool for that integration only. Clearing a namespace that was never set succeeds, which makes a rollback safe to retry.

Events

Rather than polling, subscribe to an expense event. Each delivery carries the same object documented above, receipt URL and project custom fields included, so you can act on it without a follow-up call. See the webhooks reference for the envelope, signature scheme, and retry behaviour.

TriggerTypeDescription
expense.createdeventA new expense row exists.
expense.receipt_attachedeventA receipt landed on an expense that had none.
expense.updatedeventA business field changed, or the receipt was replaced or removed.
expense.deletedeventThe expense was removed. data is null; the fields are in metadata.

If you need the receipt, listen for expense.receipt_attached

Every write path creates the expense row before it links the receipt, because the file has to reach storage before it can reference an expense id. That means expense.created usually fires with receipt: null, even when somebody photographed a receipt while filling in the form. expense.receipt_attached fires the moment the image actually lands, and its payload carries the signed URL. Expenses entered without a receipt never fire it, which is the correct behaviour for a workflow that needs one.

A reconciliation write-back never fires expense.updated. Stamping an expense in response to a delivery cannot feed that delivery back to you.