Skip to main content
This guide covers the history of expenses. Tesouro also provides document history for accounts receivable and payable history.

Overview

Tesouro keeps a history of every change made to an expense, from the moment the transaction is created to its final approval state. The history records what changed, who changed it, and when. To read the history of an expense, call GET /transactions/{transaction_id}/expense_history:
Each entry carries a timestamp, an event_type, an event_data payload whose shape follows the event type, and the fields that identify the actor:
The history of an expense stays readable after the expense is deleted. Deleting an expense does not delete its history.

Filtering and paging

This endpoint pages by offset, not by token. next_pagination_token and prev_pagination_token hold the offset to read next or previous, as a string, and null when there is no such page. Send that value back in the offset query parameter. There is no pagination_token parameter on this endpoint.

Roles and permissions

To read an expense’s history using an organization user token, the user must have a role with the transaction.read permission. If the permission type is allowed_for_own (rather than allowed), the user can read the history of their own expenses and of the expenses owned by the people who report to them. The returned history still includes the changes that other users made to those expenses. If a partner-level token is used, no special permissions are needed.

Who made the change

Every entry identifies the actor behind the event: The name and role values are captured when the entry is written and are never recalculated, so an entry shows the name and role that the user held at the time of the event, not the ones they hold today. All five are null in system entries, and the name and role fields are also null on older entries where they were not captured. Use recorded_by to tell those two cases apart.

Event types

event_type is an open vocabulary and it grows over time. Treat it as a string, and let your integration ignore a type it does not recognize rather than fail on it.
The expense status values are new, approve_in_progress, approved, rejected, and canceled.

expense_created

expense_updated

event_data.fields maps each changed field name to the change made to it. An entry with no tracked field changed carries an empty map.

submitted

approval_requested

approval_action

An approver acted on the expense. See Record an approval action for how these entries are written.

auto_approved

approvals_bypassed

An administrator force approved the expense, which cancelled the approvals that were still pending. bypassed_approver_ids is the audit fact and is always present. bypassed_approvers is a display convenience and is empty when the names could not be resolved.

status_changed

The fallback for a status transition that has no more specific event type.

reopened

A decided expense was reopened. The payload is a plain status transition and reads with the same fields as status_changed.

canceled

The expense was canceled. The payload is a plain status transition and reads with the same fields as status_changed.

deleted

The expense was deleted. Its history stays readable.

receipt_matched

receipt_unlinked

auto_categorized

payment_status_changed

The card payment behind the expense changed status. This is the payment_processing_status vocabulary, not the expense one: created, processing, succeeded, declined, refunded, and failed.

Record an approval action

Approval services record their decisions by calling POST /transactions/{transaction_id}/expense_history. The call appends one approval_action entry. The user must have a role with the transaction.approve permission. action and acted_at are required. Everything else is optional, and a field you omit is left out of the stored payload rather than stored as null.
The successful 201 response confirms the entry was recorded:
Two details are worth knowing. The entry’s timestamp is the acted_at you send, not the moment of the call, so a decision recorded late still lands in the right place in the timeline. And a repeated call for the same policy auto-approval is a no-op that still answers 201, so a retry does not create a second entry.

See also