# Batch-create expenses, and Estimate status inconsistencies

**URL:** <https://api-discuss.freeagent.com/t/batch-create-expenses-and-estimate-status-inconsistencies/6753>\
**Category:** Documentation\
**Created:** [15 September 2026 14:53 UTC](https://api-discuss.freeagent.com/t/batch-create-expenses-and-estimate-status-inconsistencies/6753 "2026-09-15T14:53:35Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![George\_JTL](https://api-discuss.freeagent.com/user_avatar/api-discuss.freeagent.com/george_jtl/32/1269_2.png) [@George\_JTL](https://api-discuss.freeagent.com/u/George_JTL)\
**Post date:** [15 September 2026 14:53 UTC](https://api-discuss.freeagent.com/t/batch-create-expenses-and-estimate-status-inconsistencies/6753/1 "2026-09-15T14:53:35Z")

</div>

We’re building an import tool against the FreeAgent v2 API (sandbox) and ran into three things worth flagging to the docs/API team - one missing documentation, two behaviours that don’t match what’s documented. All requests/responses below are exact, captured directly from a sandbox account and redacted only of account-identifying resource IDs (replaced with placeholder numbers, kept internally consistent) and access tokens.

## 1. Batch-create for Expenses has a documented request but no documented response

The [Expenses page](https://dev.freeagent.com/docs/expenses) shows a request example for creating several expenses in one call, then moves straight on to “Update an expense” - there’s no response example for the batch call at all.

Determined the actual shape empirically:

### Success

```auto
POST https://api.sandbox.freeagent.com/v2/expenses

```

Request body:

```json
{
  "expenses": [
    {
      "user": "https://api.sandbox.freeagent.com/v2/users/100001",
      "category": "https://api.sandbox.freeagent.com/v2/categories/100002",
      "dated_on": "2026-09-15",
      "gross_value": "10.00",
      "description": "Test expense A"
    },
    {
      "user": "https://api.sandbox.freeagent.com/v2/users/100001",
      "category": "https://api.sandbox.freeagent.com/v2/categories/100002",
      "dated_on": "2026-09-15",
      "gross_value": "20.00",
      "description": "Test expense B"
    }
  ]
}

```

Response - `201 Created`:

```json
{
  "expenses": [
    {
      "url": "https://api.sandbox.freeagent.com/v2/expenses/900001",
      "user": "https://api.sandbox.freeagent.com/v2/users/100001",
      "category": "https://api.sandbox.freeagent.com/v2/categories/100002",
      "dated_on": "2026-09-15",
      "currency": "GBP",
      "gross_value": "10.0",
      "native_gross_value": "10.0",
      "sales_tax_rate": "20.0",
      "sales_tax_value": "1.67",
      "native_sales_tax_value": "1.67",
      "sales_tax_status": "TAXABLE",
      "description": "Test expense A",
      "updated_at": "2026-09-15T14:31:55.000Z",
      "created_at": "2026-09-15T14:31:55.000Z"
    },
    {
      "url": "https://api.sandbox.freeagent.com/v2/expenses/900002",
      "user": "https://api.sandbox.freeagent.com/v2/users/100001",
      "category": "https://api.sandbox.freeagent.com/v2/categories/100002",
      "dated_on": "2026-09-15",
      "currency": "GBP",
      "gross_value": "20.0",
      "native_gross_value": "20.0",
      "sales_tax_rate": "20.0",
      "sales_tax_value": "3.33",
      "native_sales_tax_value": "3.33",
      "sales_tax_status": "TAXABLE",
      "description": "Test expense B",
      "updated_at": "2026-09-15T14:31:56.000Z",
      "created_at": "2026-09-15T14:31:56.000Z"
    }
  ]
}

```

One fully-populated record per submitted item, **in submission order** - safe to zip positionally back onto the request.

### Failure - atomic, no partial success

```auto
POST https://api.sandbox.freeagent.com/v2/expenses

```

Request body (first item valid, second has a blank `description` and no  
`gross_value`):

```json
{
  "expenses": [
    {
      "user": "https://api.sandbox.freeagent.com/v2/users/100001",
      "category": "https://api.sandbox.freeagent.com/v2/categories/100002",
      "dated_on": "2026-09-15",
      "gross_value": "10.00",
      "description": "Valid row"
    },
    {
      "user": "https://api.sandbox.freeagent.com/v2/users/100001",
      "category": "https://api.sandbox.freeagent.com/v2/categories/100002",
      "dated_on": "2026-09-15",
      "description": ""
    }
  ]
}

```

Response - `422 Unprocessable Entity`:

```json
{
  "errors": [
    { "message": "description can't be blank" },
    { "message": "gross_value cannot be zero" }
  ]
}

```

We confirmed by listing expenses immediately afterwards that **neither** item was created - the batch is rejected atomically, not a partial success. Both error messages came from the _second_ (invalid) item; the first (valid) item contributed nothing to the array, and nothing in the response identifies which submitted item each error belongs to. With more than two items in a batch there’s no way to tell which one(s) caused the rejection without resubmitting individually.

**Ask:** document both the success and failure response shapes here, and ideally give each error an index or reference back to the submitted item it came from.

## 2. Estimate `status` accepts fewer values at create time than documented, and silently rewrites one of them

The [Estimate Attributes table](https://dev.freeagent.com/docs/estimates) lists `status` as required, with six valid values: `Draft`, `Sent`, `Open`, `Approved`, `Rejected`, `Invoiced`. Nothing on the “Create an estimate” section suggests any restriction at create time vs. update.

Testing each value directly:

```auto
POST https://api.sandbox.freeagent.com/v2/estimates

```

**`status: "Draft"`** - created as sent:

```json
{ "estimate": { "contact": "https://api.sandbox.freeagent.com/v2/contacts/800001", "dated_on": "2026-09-15", "reference": "STATUS-TEST-Draft", "currency": "GBP", "estimate_type": "Estimate", "status": "Draft" } }

```

`201 Created`, `"status": "Draft"` in the response - as expected.

**`status: "Sent"`** - accepted, but silently changed:

```json
{ "estimate": { "contact": "https://api.sandbox.freeagent.com/v2/contacts/800002", "dated_on": "2026-09-15", "reference": "STATUS-TEST-Sent-2", "currency": "GBP", "estimate_type": "Estimate", "status": "Sent" } }

```

`201 Created`:

```json
{
  "estimate": {
    "url": "https://api.sandbox.freeagent.com/v2/estimates/700002",
    "contact": "https://api.sandbox.freeagent.com/v2/contacts/800002",
    "reference": "STATUS-TEST-Sent-2",
    "estimate_type": "Estimate",
    "dated_on": "2026-09-15",
    "status": "Open",
    "currency": "GBP",
    "net_value": "0.0",
    "total_value": "0.0",
    "contact_name": "API Test Ltd 2",
    "involves_sales_tax": false,
    "is_interim_uk_vat": false,
    "include_sales_tax_on_total_value": true,
    "updated_at": "2026-09-15T14:32:42.000Z",
    "created_at": "2026-09-15T14:32:42.000Z",
    "estimate_items": []
  }
}

```

We sent `"status": "Sent"` and got back `"status": "Open"` - `201`, no error, nothing in the response flags that the value was changed.

**`status: "Approved"`** and **`status: "Rejected"`** - both created as sent, no substitution (bodies otherwise identical to the `Sent` case above, just with `"status": "Approved"` / `"status": "Rejected"` in both request and response).

**`status: "Open"`** :

```json
{ "estimate": { "contact": "...", "dated_on": "2026-09-15", "reference": "STATUS-TEST-Open", "currency": "GBP", "estimate_type": "Estimate", "status": "Open" } }

```

`422 Unprocessable Entity`:

```json
{ "errors": [{ "message": "status is not valid" }] }

```

**`status: "Invoiced"`** - same request shape, same `422`:

```json
{ "errors": [{ "message": "status is not valid" }] }

```

So of the six documented values, only `Draft`/`Approved`/`Rejected` are created as given; `Sent` is silently rewritten to `Open`; and `Open`/`Invoiced` are rejected outright, despite being listed as valid `status` values on the same attributes table.

We suspect this connects to the “Update an estimate” section’s note that _“to update the status of an estimate you must use the status transitions to mark an estimate as Draft, Sent, Approved or Rejected”_ - which reads as being about the update endpoint, but appears to also constrain (and, for `Sent`, silently rewrite) what’s accepted at create.

One more oddity found while cleaning up test data: an estimate created with a non-`Draft` status (e.g. `Approved`) cannot be `DELETE`d directly - `DELETE /v2/estimates/:id` returns `409` with no message body beyond `"Unknown error"`. Setting `status` back to `Draft` via `PUT` first, then `DELETE`, works. This isn’t mentioned anywhere in the Estimates docs either.

**Ask:** either (a) document which of the six enum values are actually accepted at create time vs. only reachable via a status transition, and that `Sent` becomes `Open` if sent directly (or fix that to either reject or accept it as given), or (b) if this is all intentional, document it next to the attribute table and the delete endpoint respectively, rather than leaving it to be discovered by testing every value by hand.

## Environment

All requests above ran against `api.sandbox.freeagent.com` on a sandbox company created and used solely for this testing; every record created during testing (expenses, estimates, the test contacts) has since been deleted. Happy to provide anything else that would help reproduce this.

---

<div class="post-metadata">

**Author:** ![fa\_finalburn](https://api-discuss.freeagent.com/user_avatar/api-discuss.freeagent.com/fa_finalburn/32/1275_2.png) [@fa\_finalburn](https://api-discuss.freeagent.com/u/fa_finalburn)\
**Post date:** [1 October 2026 13:42 UTC](https://api-discuss.freeagent.com/t/batch-create-expenses-and-estimate-status-inconsistencies/6753/2 "2026-10-01T13:42:26Z")

</div>

Hi @George\_JTL! Thanks so much for your patience and for such a thorough report, it’s helped us to investigate and prioritise this a lot.

We’ll update the docs in the future for both of these. We’re working on an overhaul of the API docs – I’ll make sure what you’ve identified here gets factored into that.

Until the documentation is updated, we recommend:

1. Create estimates with `"status": "Draft"`.
2. Change the status with the transition endpoints.
3. To delete an estimate that isn’t in Draft, call `mark_as_draft` first, then DELETE.

Thanks again for taking the time to report this.
