> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tesouro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Expense lifecycle

> The per-expense runtime flow: auto-categorization, employee submission, approval, and accounting field auto-population.

## Overview

Once transactions are in the system, the goal is to make it as easy as possible for employees to collect receipts, fill in descriptions, and get their expenses approved. Receipt collection is the first step—employees can upload receipts directly, forward them by email, or have them auto-matched to a transaction. As soon as a receipt is matched to a transaction, Tesouro auto-fills the accounting fields, so by the time the employee opens the expense it's largely ready to submit. From there the expense moves through the approval flow and is categorized and ready for accounting export.

## Receipt collection

The first step is for the employee to upload receipts, either:

* **Direct link**: Upload the receipt and immediately link it to a specific transaction by sending `PATCH /receipts/{receipt_id}` with the `transaction_id`. Use this when the employee has the receipt in hand and knows which transaction it belongs to.
* **Auto-matching**: Upload the receipt (via file upload, or email forwarding) without specifying a transaction. The auto-matching engine finds the right transaction based on amount, merchant name, and timestamp. See [Receipts & OCR](/finops/guides/expense-management/receipts) for upload paths and matching details.

To directly link an already-uploaded receipt to a transaction:

```sh lines theme={null}
curl -X PATCH 'https://api.sandbox.tesouro.com/v1/receipts/e5ea872b-a3d6-452b-b1d3-7b4e30b8ce2b' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'X-Organization-Id: ORGANIZATION_ID' \
  -H 'X-Finops-Version: 2025-06-23' \
  -H 'Content-Type: application/json' \
  -d '{"transaction_id": "792c0577-3adf-4643-9570-2b641a5b04f2"}'
```

Once a receipt is matched — automatically or manually — Tesouro fires an enrichment step that pre-fills two fields on the transaction:

* **`ledger_account_id`**: Tesouro compares the receipt content against the organization's ledger accounts and selects the best match. Sets `source_of_data.ledger_account_id` to `"generated"`.
* **`description`**: If the transaction has no description, the AI generates a concise memo (e.g. `"Dinner in NYC"` or `"Taxi ride"`). Sets `source_of_data.description` to `"generated"`. If it can't produce a confident summary, the field is left empty.

The employee sees these values pre-filled when they open the expense and can override either before submitting. If OCR is still processing when the receipt is linked, enrichment fires after OCR completes.

<Note>
  Auto-categorization requires at least one ledger account to be configured. See [Configure accounting fields](/finops/guides/expense-management/setup#configure-accounting-fields).
</Note>

### Submit for approval

Call `POST /transactions/{id}/submit` to move the transaction from `new` to `approve_in_progress`. Tesouro enforces all configured requirements before the status changes — if any required fields are missing, the call returns a list of errors and the transaction stays in `new`:

```sh lines theme={null}
curl -X POST 'https://api.sandbox.tesouro.com/v1/transactions/792c0577-3adf-4643-9570-2b641a5b04f2/submit' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'X-Organization-Id: ORGANIZATION_ID' \
  -H 'X-Finops-Version: 2025-06-23' \
  -H 'Content-Type: application/json' \
  -d '{}'
```

If `receipt_id` is required and missing:

```json lines theme={null}
{
  "error": {
    "message": {
      "validation_errors": [
        {"field": "receipt_id", "message": "Field is required"}
      ]
    }
  }
}
```

The employee should resolve all missing requirements before submitting. A successful submit returns the updated transaction with `expense_status: "approve_in_progress"`:

```json expandable lines theme={null}
{
  "id": "792c0577-3adf-4643-9570-2b641a5b04f2",
  "expense_status": "approve_in_progress",
  "applied_policy_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "receipt_id": "e5ea872b-a3d6-452b-b1d3-7b4e30b8ce2b",
  "description": "Dinner in NYC",
  "ledger_account_id": "69e70421-23fd-4c6c-bcb5-ea3794373d44",
  "source_of_data": {
    "receipt_id": "automatched",
    "ledger_account_id": "generated",
    "description": "generated",
    "location_id": "automatched",
    "department_id": null
  }
}
```

After submit, Tesouro evaluates the active approval policies and, if one matches, sets `applied_policy_id` and notifies the designated approvers.

### Submit without a receipt

If an employee can't provide a receipt, they can bypass receipt validation by setting `bypass_receipt_validation: true` and supplying a `no_receipt_reason`. This records the exception for audit purposes:

```sh lines theme={null}
curl -X POST 'https://api.sandbox.tesouro.com/v1/transactions/792c0577-3adf-4643-9570-2b641a5b04f2/submit' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'X-Organization-Id: ORGANIZATION_ID' \
  -H 'X-Finops-Version: 2025-06-23' \
  -H 'Content-Type: application/json' \
  -d '{
    "bypass_receipt_validation": true,
    "no_receipt_reason": "Receipt was lost. Purchase was a team lunch on May 22."
  }'
```

The `no_receipt_reason` is stored on the transaction and visible to approvers. The `has_validation_errors` field on the response will be `true` to flag that the submission bypassed a requirement.

See [`POST /transactions/{id}/submit`](/finops/reference/openapi/transactions/post-transactions-id-submit) for the full API reference.

## Approval

### Policy-driven approval

When a transaction is submitted, Tesouro runs it against the active approval policies. If a policy matches:

* `applied_policy_id` is set on the transaction to identify which policy is running.
* The policy's script determines who must approve. This could be a specific user list, any user with a given role, or the employee's reporting manager.

If no policy matches, the transaction stays in `approve_in_progress` until it is force-approved or rejected by an admin.

#### List pending approval requests

The designated approver can fetch their outstanding requests:

```sh lines theme={null}
curl -X GET 'https://api.sandbox.tesouro.com/v1/approval-requests?object_type=transaction&status=waiting' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'X-Organization-Id: ORGANIZATION_ID' \
  -H 'X-Finops-Version: 2025-06-23'
```

```json expandable lines theme={null}
{
  "data": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "object_type": "transaction",
      "object_id": "792c0577-3adf-4643-9570-2b641a5b04f2",
      "status": "waiting",
      "approval_type": "user_ids",
      "required_approval_count": 1,
      "user_ids": ["FINANCE_MANAGER_USER_ID"],
      "roles": [],
      "submitted_by": "66d530d1-29b8-4c4f-93f5-fd43b3dc4b55",
      "approved_by": [],
      "rejected_by": null,
      "reject_reason": null,
      "created_by": null,
      "created_at": "2026-05-22T12:01:00.000000+00:00",
      "updated_at": "2026-05-22T12:01:00.000000+00:00"
    }
  ],
  "prev_pagination_token": null,
  "next_pagination_token": null
}
```

<Note>
  The `roles` field and role-based `approval_type` values are being refactored. Treat role-related fields as subject to change.
</Note>

#### Approve the request

```sh lines theme={null}
curl -X POST 'https://api.sandbox.tesouro.com/v1/approval-requests/a1b2c3d4-e5f6-7890-abcd-ef1234567890/approve' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'X-Organization-Id: ORGANIZATION_ID' \
  -H 'X-Finops-Version: 2025-06-23'
```

Once the required number of approvers have approved, the transaction moves to `expense_status: "approved"`. Use `GET /transactions/{id}` to confirm the final state.

### Force approve

`POST /transactions/{id}/approve` bypasses the normal policy-driven flow. Use it in two situations:

* **No policy matched**: The transaction is in `approve_in_progress` but no policy applied. Call this to approve it directly.
* **Company admin override**: A policy is running but a Company admin needs to approve immediately, regardless of the policy's designated approvers.

```sh lines theme={null}
curl -X POST 'https://api.sandbox.tesouro.com/v1/transactions/792c0577-3adf-4643-9570-2b641a5b04f2/approve' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'X-Organization-Id: ORGANIZATION_ID' \
  -H 'X-Finops-Version: 2025-06-23' \
  -H 'Content-Type: application/json'
```

The response returns the updated transaction with `expense_status: "approved"`.

### Reject a transaction

A transaction can be rejected in two ways, mirroring the two approval paths:

* **Via the approval request**: The designated approver rejects the pending approval request. Use this in the normal policy-driven flow.
* **Force reject**: An admin rejects the transaction directly, bypassing or overriding the policy. Use this when no policy applied or an admin needs to override.

#### Reject via approval request

```sh lines theme={null}
curl -X POST 'https://api.sandbox.tesouro.com/v1/approval-requests/a1b2c3d4-e5f6-7890-abcd-ef1234567890/reject' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'X-Organization-Id: ORGANIZATION_ID' \
  -H 'X-Finops-Version: 2025-06-23' \
  -H 'Content-Type: application/json' \
  -d '{"reject_reason": "Receipt is required for travel expenses over $20. Please attach the taxi receipt."}'
```

#### Force reject

```sh lines theme={null}
curl -X POST 'https://api.sandbox.tesouro.com/v1/transactions/83724fa0-ca5c-4f4e-a2eb-44c8d1d1f1c2/reject' \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'X-Organization-Id: ORGANIZATION_ID' \
  -H 'X-Finops-Version: 2025-06-23' \
  -H 'Content-Type: application/json' \
  -d '{"reject_reason": "Receipt is required for travel expenses over $20. Please attach the taxi receipt."}'
```

```json lines theme={null}
{
  "id": "83724fa0-ca5c-4f4e-a2eb-44c8d1d1f1c2",
  "expense_status": "rejected",
  "reject_reason": "Receipt is required for travel expenses over $20. Please attach the taxi receipt."
}
```

In both cases, `reject_reason` is stored on the transaction and visible to the employee. A rejected transaction is still accessible in the system, but the employee must reopen it (`POST /transactions/{id}/reopen`) before it can be submitted again.

See [`POST /transactions/{id}/reject`](/finops/reference/openapi/transactions/post-transactions-id-reject) and [`POST /approval-requests/{id}/reject`](/finops/reference/openapi/approval-requests/post-approval-requests-id-reject) for the full API reference.

## Accounting field auto-population

Transactions carry a `source_of_data` JSON object that records how each field was set. Fields like `ledger_account_id`, `description`, `receipt_id`, `location_id`, and `department_id` are all auto-populated but overridable — employees and admins can update any of them, and the entry in `source_of_data` updates to `"user"` to reflect that.

### source\_of\_data on transactions

Each entry in the `source_of_data` object takes one of these values:

| Value           | Meaning                                                                                                                                                 |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"user"`        | The employee or admin explicitly set this value.                                                                                                        |
| `"generated"`   | Auto-generated by Tesouro when a receipt was matched (e.g., `ledger_account_id` and `description`).                                                     |
| `"automatched"` | Set automatically by Tesouro — either by the AI receipt matching engine (`receipt_id`) or from the employee's profile (`location_id`, `department_id`). |
| `null`          | This field hasn't been set yet.                                                                                                                         |

`location_id` and `department_id` are auto-assigned from the employee's user profile when the transaction is created. When an employee edits only one field, the other fields' `source_of_data` entries are preserved — only the changed field's origin updates to `"user"`.

### source\_of\_data on receipts

Receipts have their own `source_of_data` field that records how the receipt's data was captured — separate from the transaction tracking above:

| Value              | Meaning                                                                                            |
| ------------------ | -------------------------------------------------------------------------------------------------- |
| `"ocr"`            | The receipt was created from a file upload. Tesouro's OCR service extracted the structured fields. |
| `"user_specified"` | The receipt was created by sending structured data directly to `POST /receipts` — no file, no OCR. |

When the AI auto-matching engine links a receipt to a transaction, it sets `source_of_data.receipt_id` to `"automatched"` on the transaction. A manual match sets it to `"user"`. This distinction is shown in the expense management UI so employees can see at a glance whether a receipt was attached automatically or still needs their attention.
