> This is the Markdown of https://backed.giving/developers/reference/files, a page of the Backed developer documentation. Every page is listed at https://backed.giving/developers/llms.txt.

# Files

The three-step file upload. 3 operations: parameters, request body and answers, with examples.

- [POST /api/v2/files](#reserveFileUpload) — Reserve a file upload.
- [POST /api/v2/files/{id}/attach](#attachFileUpload) — Attach a scanned file to its target.
- [POST /api/v2/files/{id}/confirm](#confirmFileUpload) — Confirm a file upload.

<a id="reserveFileUpload"></a>
## POST /api/v2/files

**Reserve a file upload.**

Step 1 of 3. The target is checked and authorised as the product’s own action on it, the size and content type against its limits, all before anything is stored; then a 15-minute presigned upload is answered.

- Operation ID: `reserveFileUpload`
- Authentication: A Bearer token from a credential holding the `write:files` scope. Runs as the credential’s Administrator, and needs an `Idempotency-Key` header.

Example request:

```bash
curl -X POST "https://your-org.backed.giving/api/v2/files" \
  -H "Authorization: Bearer $BACKED_TOKEN" \
  -H "Idempotency-Key: 7d1f0c2e-5b8a-4f3e-9c61-2a4b8e9d0f13" \
  -H "Content-Type: application/json" \
  -d '{"filename":"final-report.pdf","content_type":"application/pdf","size":482113,"target":{"type":"milestone_deliverable","id":2210,"field":950}}'
```

### Parameters

Parameters of POST /api/v2/files:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | Yes | `string` | A key of 1 to 255 characters, unique per write. A retry with the same key, method, path and body within 24 hours answers the first response again (with `Idempotent-Replayed: true`) and writes nothing. Example: `7d1f0c2e-5b8a-4f3e-9c61-2a4b8e9d0f13`. |

### Request body

`application/json`, the `FileUploadRequest` schema. The file to reserve and where it goes: a milestone deliverable (with its File upload `field`), an application note or a registration entity.

Fields of the FileUploadRequest body:

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `filename` | `string` | Yes |  |
| `content_type` | `string` | Yes |  |
| `size` | `integer` | Yes |  |
| `target` | `object` | Yes |  |

Example request body:

```json
{
  "filename": "final-report.pdf",
  "content_type": "application/pdf",
  "size": 482113,
  "target": {
    "type": "milestone_deliverable",
    "id": 2210,
    "field": 950
  }
}
```

<a id="reserveFileUpload-201"></a>
### 201 response

The upload id and presigned upload.

Headers: `Idempotent-Replayed`.

Fields of FileUploadReserved:

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `id` | `string` | Yes |  |
| `upload` | `object` | Yes |  |
| `expires_at` | `string` | Yes |  |
| `max_bytes` | `integer` | Yes |  |
| `state` | `string` | Yes | Always `awaiting_upload`. |

Example 201 response:

```json
{
  "id": "upl_3Jq9xVb2LmT8sKp4Rz7Wn1Ye",
  "upload": {
    "url": "https://uploads.example.com/backed",
    "method": "POST",
    "fields": {
      "key": "tmp/2026/11/09/3f0c9d1e",
      "policy": "eyJleHBpcmF0aW9uIjoi...",
      "x-amz-signature": "9c1f4e..."
    },
    "headers": {}
  },
  "expires_at": "2026-11-09T02:45:00.000Z",
  "max_bytes": 104857600,
  "state": "awaiting_upload"
}
```

<a id="reserveFileUpload-400"></a>
### 400 response

`idempotency_key_required`: The write has no Idempotency-Key header of 1 to 255 characters. `invalid_request`: A query parameter is invalid, or a write’s body is not a JSON object.

Example 400 response: idempotency_key_required:

```json
{
  "error": "idempotency_key_required",
  "error_description": "A write needs an Idempotency-Key header of 1 to 255 characters"
}
```

Example 400 response: invalid_request:

```json
{
  "error": "invalid_request",
  "error_description": "limit is invalid"
}
```

<a id="reserveFileUpload-401"></a>
### 401 response

The token is missing, invalid, expired or revoked, of another Organisation, or API access is not Full. Ask for a new token.

Headers: `WWW-Authenticate`.

Example 401 response:

```json
{
  "error": "invalid_token",
  "error_description": "The access token is invalid or has expired"
}
```

<a id="reserveFileUpload-403"></a>
### 403 response

`https_required`: The request was sent over plain HTTP and was not processed. `insufficient_scope`: The credential does not hold the scope the route needs. `run_as_disabled`: The credential has no run-as Administrator, or that person is no longer one. `forbidden`: The run-as Administrator lacks the permission, or the record is outside their Data Access Rules.

Headers: `WWW-Authenticate`.

Example 403 response: https_required:

```json
{
  "error": "https_required"
}
```

Example 403 response: insufficient_scope:

```json
{
  "error": "insufficient_scope",
  "error_description": "This credential lacks the write:applications scope"
}
```

Example 403 response: run_as_disabled:

```json
{
  "error": "run_as_disabled",
  "error_description": "This credential has no enabled run-as Administrator"
}
```

Example 403 response: forbidden:

```json
{
  "error": "forbidden",
  "error_description": "The run-as Administrator may not do this to this record"
}
```

<a id="reserveFileUpload-404"></a>
### 404 response

An unknown host or path, or an id that is unknown, deleted or of another Organisation.

Example 404 response:

```json
{
  "error": "not_found"
}
```

<a id="reserveFileUpload-409"></a>
### 409 response

`idempotency_in_progress`: The first request with this Idempotency-Key has not finished. Nothing was written; retry after the given seconds. `invalid_state`: The record is not in a state that admits this write.

Headers: `Retry-After`.

Example 409 response: idempotency_in_progress:

```json
{
  "error": "idempotency_in_progress",
  "error_description": "The first request with this Idempotency-Key has not finished. If this persists it stopped part-way: check whether the record exists before writing again with a new key"
}
```

Example 409 response: invalid_state:

```json
{
  "error": "invalid_state",
  "error_description": "The target does not take files in its current state"
}
```

<a id="reserveFileUpload-413"></a>
### 413 response

The body, or the declared file, is larger than the limit that applies.

Example 413 response:

```json
{
  "error": "payload_too_large",
  "error_description": "The request body is too large"
}
```

<a id="reserveFileUpload-415"></a>
### 415 response

The body is not application/json, or the target does not accept the file’s content type.

Example 415 response:

```json
{
  "error": "unsupported_media_type",
  "error_description": "The request body must be application/json"
}
```

<a id="reserveFileUpload-422"></a>
### 422 response

`idempotency_key_reused`: The Idempotency-Key was used in the last 24 hours for a different method, path or body. `unknown_field`: The body has a property the route does not document; the first is named. `validation_failed`: Body values the product’s own form would refuse; `fields` names each.

Example 422 response: idempotency_key_reused:

```json
{
  "error": "idempotency_key_reused",
  "error_description": "This Idempotency-Key was used for a different request"
}
```

Example 422 response: unknown_field:

```json
{
  "error": "unknown_field",
  "error_description": "bank_account_number is not a writable field"
}
```

Example 422 response: validation_failed:

```json
{
  "error": "validation_failed",
  "error_description": "Invalid fields: due_date",
  "fields": [
    "due_date"
  ]
}
```

<a id="reserveFileUpload-429"></a>
### 429 response

A rate limit was exceeded: 120 requests a minute per credential, 30 token requests a minute per client\_id.

Headers: `Retry-After`.

Example 429 response:

```json
{
  "error": "rate_limited",
  "error_description": "Too many requests"
}
```

<a id="reserveFileUpload-500"></a>
### 500 response

An unexpected server error.

Example 500 response:

```json
{
  "error": "server_error"
}
```

<a id="reserveFileUpload-503"></a>
### 503 response

The Organisation’s API access cannot be determined, or a dependency is unavailable. Retry after the given seconds.

Headers: `Retry-After`.

Example 503 response:

```json
{
  "error": "temporarily_unavailable"
}
```

<a id="attachFileUpload"></a>
## POST /api/v2/files/{id}/attach

**Attach a scanned file to its target.**

Step 3 of 3. Only a clean file is attached: still scanning is 409 `file_not_scanned`; rejected by the scan is 422 `file_infected`. No body. A repeat answers the same attachment.

- Operation ID: `attachFileUpload`
- Authentication: A Bearer token from a credential holding the `write:files` scope. Runs as the credential’s Administrator, and needs an `Idempotency-Key` header.

Example request:

```bash
curl -X POST "https://your-org.backed.giving/api/v2/files/upl_3Jq9xVb2LmT8sKp4Rz7Wn1Ye/attach" \
  -H "Authorization: Bearer $BACKED_TOKEN" \
  -H "Idempotency-Key: 7d1f0c2e-5b8a-4f3e-9c61-2a4b8e9d0f13"
```

### Parameters

Parameters of POST /api/v2/files/{id}/attach:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | Yes | `string` | The opaque upload id `POST /api/v2/files` answered, for the credential that reserved it. Example: `upl_3Jq9xVb2LmT8sKp4Rz7Wn1Ye`. |
| `Idempotency-Key` | header | Yes | `string` | A key of 1 to 255 characters, unique per write. A retry with the same key, method, path and body within 24 hours answers the first response again (with `Idempotent-Replayed: true`) and writes nothing. Example: `7d1f0c2e-5b8a-4f3e-9c61-2a4b8e9d0f13`. |

<a id="attachFileUpload-200"></a>
### 200 response

The attachment.

Headers: `Idempotent-Replayed`.

Fields of FileAttached:

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `id` | `integer` | Yes |  |
| `state` | `string` | Yes | Always `attached`. |
| `target` | `object` | Yes |  |

Example 200 response:

```json
{
  "id": 99120,
  "state": "attached",
  "target": {
    "type": "milestone_deliverable",
    "id": 2210,
    "field": 950
  }
}
```

<a id="attachFileUpload-400"></a>
### 400 response

The write has no Idempotency-Key header of 1 to 255 characters.

Example 400 response:

```json
{
  "error": "idempotency_key_required",
  "error_description": "A write needs an Idempotency-Key header of 1 to 255 characters"
}
```

<a id="attachFileUpload-401"></a>
### 401 response

The token is missing, invalid, expired or revoked, of another Organisation, or API access is not Full. Ask for a new token.

Headers: `WWW-Authenticate`.

Example 401 response:

```json
{
  "error": "invalid_token",
  "error_description": "The access token is invalid or has expired"
}
```

<a id="attachFileUpload-403"></a>
### 403 response

`https_required`: The request was sent over plain HTTP and was not processed. `insufficient_scope`: The credential does not hold the scope the route needs. `run_as_disabled`: The credential has no run-as Administrator, or that person is no longer one. `forbidden`: The run-as Administrator lacks the permission, or the record is outside their Data Access Rules.

Headers: `WWW-Authenticate`.

Example 403 response: https_required:

```json
{
  "error": "https_required"
}
```

Example 403 response: insufficient_scope:

```json
{
  "error": "insufficient_scope",
  "error_description": "This credential lacks the write:applications scope"
}
```

Example 403 response: run_as_disabled:

```json
{
  "error": "run_as_disabled",
  "error_description": "This credential has no enabled run-as Administrator"
}
```

Example 403 response: forbidden:

```json
{
  "error": "forbidden",
  "error_description": "The run-as Administrator may not do this to this record"
}
```

<a id="attachFileUpload-404"></a>
### 404 response

An unknown host or path, or an id that is unknown, deleted or of another Organisation.

Example 404 response:

```json
{
  "error": "not_found"
}
```

<a id="attachFileUpload-409"></a>
### 409 response

`idempotency_in_progress`: The first request with this Idempotency-Key has not finished. Nothing was written; retry after the given seconds. `file_not_scanned`: The file has not finished its malware scan; try the attach again shortly. `invalid_state`: The record is not in a state that admits this write.

Headers: `Retry-After`.

Example 409 response: idempotency_in_progress:

```json
{
  "error": "idempotency_in_progress",
  "error_description": "The first request with this Idempotency-Key has not finished. If this persists it stopped part-way: check whether the record exists before writing again with a new key"
}
```

Example 409 response: file_not_scanned:

```json
{
  "error": "file_not_scanned",
  "error_description": "The file has not finished its malware scan"
}
```

Example 409 response: invalid_state:

```json
{
  "error": "invalid_state",
  "error_description": "The target does not take files in its current state"
}
```

<a id="attachFileUpload-413"></a>
### 413 response

The body, or the declared file, is larger than the limit that applies.

Example 413 response:

```json
{
  "error": "payload_too_large",
  "error_description": "The request body is too large"
}
```

<a id="attachFileUpload-415"></a>
### 415 response

The body is not application/json, or the target does not accept the file’s content type.

Example 415 response:

```json
{
  "error": "unsupported_media_type",
  "error_description": "The request body must be application/json"
}
```

<a id="attachFileUpload-422"></a>
### 422 response

`idempotency_key_reused`: The Idempotency-Key was used in the last 24 hours for a different method, path or body. `file_infected`: The file failed its malware scan and was not attached.

Example 422 response: idempotency_key_reused:

```json
{
  "error": "idempotency_key_reused",
  "error_description": "This Idempotency-Key was used for a different request"
}
```

Example 422 response: file_infected:

```json
{
  "error": "file_infected",
  "error_description": "The file failed its malware scan"
}
```

<a id="attachFileUpload-429"></a>
### 429 response

A rate limit was exceeded: 120 requests a minute per credential, 30 token requests a minute per client\_id.

Headers: `Retry-After`.

Example 429 response:

```json
{
  "error": "rate_limited",
  "error_description": "Too many requests"
}
```

<a id="attachFileUpload-500"></a>
### 500 response

An unexpected server error.

Example 500 response:

```json
{
  "error": "server_error"
}
```

<a id="attachFileUpload-503"></a>
### 503 response

The Organisation’s API access cannot be determined, or a dependency is unavailable. Retry after the given seconds.

Headers: `Retry-After`.

Example 503 response:

```json
{
  "error": "temporarily_unavailable"
}
```

<a id="confirmFileUpload"></a>
## POST /api/v2/files/{id}/confirm

**Confirm a file upload.**

Step 2 of 3, after the bytes are uploaded: the file is recorded and its malware scan begins. No body. Not yet uploaded: 409 `invalid_state`.

- Operation ID: `confirmFileUpload`
- Authentication: A Bearer token from a credential holding the `write:files` scope. Runs as the credential’s Administrator, and needs an `Idempotency-Key` header.

Example request:

```bash
curl -X POST "https://your-org.backed.giving/api/v2/files/upl_3Jq9xVb2LmT8sKp4Rz7Wn1Ye/confirm" \
  -H "Authorization: Bearer $BACKED_TOKEN" \
  -H "Idempotency-Key: 7d1f0c2e-5b8a-4f3e-9c61-2a4b8e9d0f13"
```

### Parameters

Parameters of POST /api/v2/files/{id}/confirm:

| Name | In | Required | Type | Description |
| --- | --- | --- | --- | --- |
| `id` | path | Yes | `string` | The opaque upload id `POST /api/v2/files` answered, for the credential that reserved it. Example: `upl_3Jq9xVb2LmT8sKp4Rz7Wn1Ye`. |
| `Idempotency-Key` | header | Yes | `string` | A key of 1 to 255 characters, unique per write. A retry with the same key, method, path and body within 24 hours answers the first response again (with `Idempotent-Replayed: true`) and writes nothing. Example: `7d1f0c2e-5b8a-4f3e-9c61-2a4b8e9d0f13`. |

<a id="confirmFileUpload-200"></a>
### 200 response

The scan has begun.

Headers: `Idempotent-Replayed`.

Fields of FileConfirmed:

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `id` | `string` | Yes |  |
| `state` | `string` | Yes | Always `scanning`. |

Example 200 response:

```json
{
  "id": "upl_3Jq9xVb2LmT8sKp4Rz7Wn1Ye",
  "state": "scanning"
}
```

<a id="confirmFileUpload-400"></a>
### 400 response

The write has no Idempotency-Key header of 1 to 255 characters.

Example 400 response:

```json
{
  "error": "idempotency_key_required",
  "error_description": "A write needs an Idempotency-Key header of 1 to 255 characters"
}
```

<a id="confirmFileUpload-401"></a>
### 401 response

The token is missing, invalid, expired or revoked, of another Organisation, or API access is not Full. Ask for a new token.

Headers: `WWW-Authenticate`.

Example 401 response:

```json
{
  "error": "invalid_token",
  "error_description": "The access token is invalid or has expired"
}
```

<a id="confirmFileUpload-403"></a>
### 403 response

`https_required`: The request was sent over plain HTTP and was not processed. `insufficient_scope`: The credential does not hold the scope the route needs. `run_as_disabled`: The credential has no run-as Administrator, or that person is no longer one. `forbidden`: The run-as Administrator lacks the permission, or the record is outside their Data Access Rules.

Headers: `WWW-Authenticate`.

Example 403 response: https_required:

```json
{
  "error": "https_required"
}
```

Example 403 response: insufficient_scope:

```json
{
  "error": "insufficient_scope",
  "error_description": "This credential lacks the write:applications scope"
}
```

Example 403 response: run_as_disabled:

```json
{
  "error": "run_as_disabled",
  "error_description": "This credential has no enabled run-as Administrator"
}
```

Example 403 response: forbidden:

```json
{
  "error": "forbidden",
  "error_description": "The run-as Administrator may not do this to this record"
}
```

<a id="confirmFileUpload-404"></a>
### 404 response

An unknown host or path, or an id that is unknown, deleted or of another Organisation.

Example 404 response:

```json
{
  "error": "not_found"
}
```

<a id="confirmFileUpload-409"></a>
### 409 response

`idempotency_in_progress`: The first request with this Idempotency-Key has not finished. Nothing was written; retry after the given seconds. `invalid_state`: The record is not in a state that admits this write.

Headers: `Retry-After`.

Example 409 response: idempotency_in_progress:

```json
{
  "error": "idempotency_in_progress",
  "error_description": "The first request with this Idempotency-Key has not finished. If this persists it stopped part-way: check whether the record exists before writing again with a new key"
}
```

Example 409 response: invalid_state:

```json
{
  "error": "invalid_state",
  "error_description": "The target does not take files in its current state"
}
```

<a id="confirmFileUpload-413"></a>
### 413 response

The body, or the declared file, is larger than the limit that applies.

Example 413 response:

```json
{
  "error": "payload_too_large",
  "error_description": "The request body is too large"
}
```

<a id="confirmFileUpload-415"></a>
### 415 response

The body is not application/json, or the target does not accept the file’s content type.

Example 415 response:

```json
{
  "error": "unsupported_media_type",
  "error_description": "The request body must be application/json"
}
```

<a id="confirmFileUpload-422"></a>
### 422 response

The Idempotency-Key was used in the last 24 hours for a different method, path or body.

Example 422 response:

```json
{
  "error": "idempotency_key_reused",
  "error_description": "This Idempotency-Key was used for a different request"
}
```

<a id="confirmFileUpload-429"></a>
### 429 response

A rate limit was exceeded: 120 requests a minute per credential, 30 token requests a minute per client\_id.

Headers: `Retry-After`.

Example 429 response:

```json
{
  "error": "rate_limited",
  "error_description": "Too many requests"
}
```

<a id="confirmFileUpload-500"></a>
### 500 response

An unexpected server error.

Example 500 response:

```json
{
  "error": "server_error"
}
```

<a id="confirmFileUpload-503"></a>
### 503 response

The Organisation’s API access cannot be determined, or a dependency is unavailable. Retry after the given seconds.

Headers: `Retry-After`.

Example 503 response:

```json
{
  "error": "temporarily_unavailable"
}
```
