# Files endpoints

> Two-phase file upload to a run's File fields, and signed downloads. Every operation with parameters, body, responses, errors and examples.

Source: https://developers.processcart.com/reference/files

Two-phase file upload to a run's File fields, and signed downloads.

Base URL `https://app.processcart.com/api/v1`. Every operation below is also in the [OpenAPI document](https://developers.processcart.com/openapi.json) and the [interactive reference](https://developers.processcart.com/reference).

| Operation | Method and path | Scope |
| --- | --- | --- |
| [Request an upload ticket](#requestRunFile) | <code class="pc-docs-wrap">POST /v1<wbr>/runs<wbr>/{id}<wbr>/files</code> | Scope `runs:write` |
| [Finalize an upload](#finalizeRunFile) | <code class="pc-docs-wrap">POST /v1<wbr>/runs<wbr>/{id}<wbr>/files<wbr>/{attachmentId}<wbr>/finalize</code> | Scope `runs:write` |
| [List a run's files](#listRunFiles) | <code class="pc-docs-wrap">GET /v1<wbr>/runs<wbr>/{id}<wbr>/files</code> | Scope `runs:read` |
| [Get a file with a download link](#getRunFile) | <code class="pc-docs-wrap">GET /v1<wbr>/runs<wbr>/{id}<wbr>/files<wbr>/{attachmentId}</code> | Scope `runs:read` |
| [Remove a file](#deleteRunFile) | <code class="pc-docs-wrap">DELETE /v1<wbr>/runs<wbr>/{id}<wbr>/files<wbr>/{attachmentId}</code> | Scope `runs:write` |

## POST /v1/runs/{id}/files
**Request an upload ticket.** Scope `runs:write` · checked against the key's workflow / folder allowlist (403 `not_allowed_for_key` outside it) · accepts `Idempotency-Key`. Operation id `requestRunFile` · [open in the API reference](https://developers.processcart.com/reference#tag/files/POST/v1/runs/{id}/files).

Step 1 of 3. Names the file, its size and type and the File field it belongs to; answers 200 with a signed `upload.url` to PUT the raw bytes to (two hours, no other credential) and the `finalize` call to make afterwards. Nothing is stored yet — an abandoned ticket leaves no trace on the run. A hidden field, a non-File field or a comment target is refused. An `Idempotency-Key` replay past the ticket's two hours hands back an expired URL: request a new ticket under a new key.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |

**Request headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | no | Makes the request safe to retry: a second request with the same key and a byte-identical body (and the same `Expected-Revision`) replays the stored response (`Idempotent-Replayed: true`); a different body with the same key is refused (422 `idempotency_key_reused`) and no second resource is created. Keys are stored per workspace for 24 hours and replayed only to the API key that made the request (or the key one rotation away from it); another API key sending the same value is refused (422 `idempotency_key_reused`), so prefix keys per integration. 409, 429 and 5xx answers are never stored. |

**Request body** — `application/json`, [RequestRunFileBody](https://developers.processcart.com/reference/schemas#requestrunfilebody)

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `filename` | string | yes | The display name. Send the SAME string again at finalize. |
| `size` | integer | yes | The byte size (at most 26214400 — 25 MB). |
| `contentType` | string | yes | The MIME type you will send as the PUT's Content-Type — it becomes the stored type (echoed in `upload.headers`). |
| `target` | object | yes |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| 200 | OK. | [UploadTicket](https://developers.processcart.com/reference/schemas#uploadticket) |
| 401 | No usable credential. `unauthorized` — Authentication required; `invalid_api_key` — Invalid API key; `key_revoked` — API key revoked. | [Problem](https://developers.processcart.com/errors) |
| 403 | `insufficient_scope` — Insufficient scope; `not_allowed_for_key` — Not allowed for this key; `read_only_key` — Read-only key. | [Problem](https://developers.processcart.com/errors) |
| 404 | `not_found` — Not found. | [Problem](https://developers.processcart.com/errors) |
| 405 | The verb is not supported on this path. `method_not_allowed` — Method not allowed. | [Problem](https://developers.processcart.com/errors) |
| 409 | `idempotency_in_progress` — Request in progress; `run_archived` — Run is archived; `run_trashed` — Run is in Trash. | [Problem](https://developers.processcart.com/errors) |
| 413 | `payload_too_large` — Payload too large. | [Problem](https://developers.processcart.com/errors) |
| 415 | `unsupported_media_type` — Unsupported media type. | [Problem](https://developers.processcart.com/errors) |
| 422 | `field_not_writable` — Field is not writable; `idempotency_key_reused` — Idempotency key reused; `validation_failed` — Validation failed. | [Problem](https://developers.processcart.com/errors) |
| 429 | `rate_limited` — Too many requests. | [Problem](https://developers.processcart.com/errors) |
| 500 | `internal_error` — Internal error. | [Problem](https://developers.processcart.com/errors) |
| 503 | `unavailable` — Service unavailable. | [Problem](https://developers.processcart.com/errors) |

**Errors** (every code this operation can answer; see [Errors](https://developers.processcart.com/errors))

- 401 [`invalid_api_key`](https://developers.processcart.com/errors#invalid_api_key) — Invalid API key
- 401 [`key_revoked`](https://developers.processcart.com/errors#key_revoked) — API key revoked
- 401 [`unauthorized`](https://developers.processcart.com/errors#unauthorized) — Authentication required
- 403 [`insufficient_scope`](https://developers.processcart.com/errors#insufficient_scope) — Insufficient scope
- 403 [`not_allowed_for_key`](https://developers.processcart.com/errors#not_allowed_for_key) — Not allowed for this key
- 403 [`read_only_key`](https://developers.processcart.com/errors#read_only_key) — Read-only key
- 404 [`not_found`](https://developers.processcart.com/errors#not_found) — Not found
- 405 [`method_not_allowed`](https://developers.processcart.com/errors#method_not_allowed) — Method not allowed
- 409 [`idempotency_in_progress`](https://developers.processcart.com/errors#idempotency_in_progress) — Request in progress
- 409 [`run_archived`](https://developers.processcart.com/errors#run_archived) — Run is archived
- 409 [`run_trashed`](https://developers.processcart.com/errors#run_trashed) — Run is in Trash
- 413 [`payload_too_large`](https://developers.processcart.com/errors#payload_too_large) — Payload too large
- 415 [`unsupported_media_type`](https://developers.processcart.com/errors#unsupported_media_type) — Unsupported media type
- 422 [`field_not_writable`](https://developers.processcart.com/errors#field_not_writable) — Field is not writable
- 422 [`idempotency_key_reused`](https://developers.processcart.com/errors#idempotency_key_reused) — Idempotency key reused
- 422 [`validation_failed`](https://developers.processcart.com/errors#validation_failed) — Validation failed
- 429 [`rate_limited`](https://developers.processcart.com/errors#rate_limited) — Too many requests
- 500 [`internal_error`](https://developers.processcart.com/errors#internal_error) — Internal error
- 503 [`unavailable`](https://developers.processcart.com/errors#unavailable) — Service unavailable

**Example request**

```json
{
  "filename": " contract.pdf ",
  "size": 1234,
  "contentType": "application/pdf",
  "target": {
    "kind": "field",
    "fieldKey": "contract"
  }
}
```

**Example response** (200)

```json
{
  "attachmentId": "03f9307d-039e-484d-8da4-a460186ede29",
  "upload": {
    "method": "PUT",
    "url": "https://storage.example.com/upload/REDACTED",
    "headers": {
      "Content-Type": "application/pdf"
    },
    "expiresAt": "2026-10-13T03:09:40.975Z"
  },
  "finalize": {
    "method": "POST",
    "url": "https://app.processcart.com/api/v1/runs/3245e6a5-3fbe-463a-ad0b-a7fd871957b0/files/03f9307d-039e-484d-8da4-a460186ede29/finalize",
    "body": {
      "filename": "contract.pdf",
      "target": {
        "kind": "field",
        "fieldKey": "contract"
      }
    }
  }
}
```


## POST /v1/runs/{id}/files/{attachmentId}/finalize
**Finalize an upload.** Scope `runs:write` · checked against the key's workflow / folder allowlist (403 `not_allowed_for_key` outside it) · accepts `Idempotency-Key` · accepts `Expected-Revision` (409 `revision_mismatch` when stale). Operation id `finalizeRunFile` · [open in the API reference](https://developers.processcart.com/reference#tag/files/POST/v1/runs/{id}/files/{attachmentId}/finalize).

Step 3 of 3, after the PUT. Verifies the stored object (its size and type are read from Storage, never from you), records the attachment and writes it into the File field under the run lock — a single-file field replaces its current file, a multi-file field appends. Answers the attachment beside the updated Run (its new `revision` is `run.revision`); `Expected-Revision` is honored. Before the bytes arrived it answers 409 `upload_incomplete`; a field of a completed task must be reopened first. Finalizing the same ticket twice is a no-op.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `attachmentId` | string (uuid) | yes |  |

**Request headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | no | Makes the request safe to retry: a second request with the same key and a byte-identical body (and the same `Expected-Revision`) replays the stored response (`Idempotent-Replayed: true`); a different body with the same key is refused (422 `idempotency_key_reused`) and no second resource is created. Keys are stored per workspace for 24 hours and replayed only to the API key that made the request (or the key one rotation away from it); another API key sending the same value is refused (422 `idempotency_key_reused`), so prefix keys per integration. 409, 429 and 5xx answers are never stored. |
| `Expected-Revision` | string | no | Optimistic concurrency: the run's `revision` you last saw (from any Run body) — a bare integer (`7`) or the same in double quotes (`"7"`); `*` or no header = no precondition. Checked INSIDE the run lock — a stale value is refused with 409 `revision_mismatch` whose body names `currentRevision`. A list or any other value is a 422. A successful write answers its new `revision` in the body. The API sends no `ETag` and refuses `If-Match` (422): CDN layers evaluate `If-Match` against the response and turn a successful write into a 412. |

**Request body** — `application/json`, [FinalizeRunFileBody](https://developers.processcart.com/reference/schemas#finalizerunfilebody)

| Property | Type | Required | Description |
| --- | --- | --- | --- |
| `filename` | string | yes |  |
| `target` | object | yes |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| 200 | OK. | [FinalizeRunFileResponse](https://developers.processcart.com/reference/schemas#finalizerunfileresponse) |
| 401 | No usable credential. `unauthorized` — Authentication required; `invalid_api_key` — Invalid API key; `key_revoked` — API key revoked. | [Problem](https://developers.processcart.com/errors) |
| 403 | `insufficient_scope` — Insufficient scope; `not_allowed_for_key` — Not allowed for this key; `read_only_key` — Read-only key. | [Problem](https://developers.processcart.com/errors) |
| 404 | `not_found` — Not found. | [Problem](https://developers.processcart.com/errors) |
| 405 | The verb is not supported on this path. `method_not_allowed` — Method not allowed. | [Problem](https://developers.processcart.com/errors) |
| 409 | `idempotency_in_progress` — Request in progress; `revision_mismatch` — Revision mismatch; `run_archived` — Run is archived; `run_trashed` — Run is in Trash; `task_completed` — Task already completed; `upload_incomplete` — Upload incomplete. | [Problem](https://developers.processcart.com/errors) |
| 413 | `payload_too_large` — Payload too large. | [Problem](https://developers.processcart.com/errors) |
| 415 | `unsupported_media_type` — Unsupported media type. | [Problem](https://developers.processcart.com/errors) |
| 422 | `field_not_writable` — Field is not writable; `idempotency_key_reused` — Idempotency key reused; `validation_failed` — Validation failed. | [Problem](https://developers.processcart.com/errors) |
| 429 | `rate_limited` — Too many requests. | [Problem](https://developers.processcart.com/errors) |
| 500 | `internal_error` — Internal error. | [Problem](https://developers.processcart.com/errors) |
| 503 | `unavailable` — Service unavailable. | [Problem](https://developers.processcart.com/errors) |

**Errors** (every code this operation can answer; see [Errors](https://developers.processcart.com/errors))

- 401 [`invalid_api_key`](https://developers.processcart.com/errors#invalid_api_key) — Invalid API key
- 401 [`key_revoked`](https://developers.processcart.com/errors#key_revoked) — API key revoked
- 401 [`unauthorized`](https://developers.processcart.com/errors#unauthorized) — Authentication required
- 403 [`insufficient_scope`](https://developers.processcart.com/errors#insufficient_scope) — Insufficient scope
- 403 [`not_allowed_for_key`](https://developers.processcart.com/errors#not_allowed_for_key) — Not allowed for this key
- 403 [`read_only_key`](https://developers.processcart.com/errors#read_only_key) — Read-only key
- 404 [`not_found`](https://developers.processcart.com/errors#not_found) — Not found
- 405 [`method_not_allowed`](https://developers.processcart.com/errors#method_not_allowed) — Method not allowed
- 409 [`idempotency_in_progress`](https://developers.processcart.com/errors#idempotency_in_progress) — Request in progress
- 409 [`revision_mismatch`](https://developers.processcart.com/errors#revision_mismatch) — Revision mismatch
- 409 [`run_archived`](https://developers.processcart.com/errors#run_archived) — Run is archived
- 409 [`run_trashed`](https://developers.processcart.com/errors#run_trashed) — Run is in Trash
- 409 [`task_completed`](https://developers.processcart.com/errors#task_completed) — Task already completed
- 409 [`upload_incomplete`](https://developers.processcart.com/errors#upload_incomplete) — Upload incomplete
- 413 [`payload_too_large`](https://developers.processcart.com/errors#payload_too_large) — Payload too large
- 415 [`unsupported_media_type`](https://developers.processcart.com/errors#unsupported_media_type) — Unsupported media type
- 422 [`field_not_writable`](https://developers.processcart.com/errors#field_not_writable) — Field is not writable
- 422 [`idempotency_key_reused`](https://developers.processcart.com/errors#idempotency_key_reused) — Idempotency key reused
- 422 [`validation_failed`](https://developers.processcart.com/errors#validation_failed) — Validation failed
- 429 [`rate_limited`](https://developers.processcart.com/errors#rate_limited) — Too many requests
- 500 [`internal_error`](https://developers.processcart.com/errors#internal_error) — Internal error
- 503 [`unavailable`](https://developers.processcart.com/errors#unavailable) — Service unavailable

**Example request**

```json
{
  "filename": "contract.pdf",
  "target": {
    "kind": "field",
    "fieldKey": "contract"
  }
}
```

**Example response** (200)

```json
{
  "attachment": {
    "id": "03f9307d-039e-484d-8da4-a460186ede29",
    "runId": "3245e6a5-3fbe-463a-ad0b-a7fd871957b0",
    "taskKey": "details",
    "fieldKey": "contract",
    "commentId": null,
    "filename": "contract.pdf",
    "size": 1234,
    "contentType": "application/pdf",
    "uploadedByUserId": null,
    "uploadedBy": {
      "kind": "api",
      "keyId": "d422b49f-a3f2-4244-b97e-8d50f25c3036",
      "name": "collab key"
    },
    "createdAt": "2026-10-13T01:09:44.456Z"
  },
  "run": {
    "id": "3245e6a5-3fbe-463a-ad0b-a7fd871957b0",
    "name": "Collab run",
    "status": "active",
    "priority": "none",
    "labels": [],
    "labelDetails": [],
    "dueAt": null,
    "keyDate": null,
    "revision": 4,
    "updatedAt": "2026-10-13T01:09:44.456Z",
    "startedAt": "2026-10-13T01:09:28.312Z",
    "startedBy": "api",
    "startedByUserId": null,
    "schedule": null,
    "scheduledFor": null,
    "completedAt": null,
    "deletedAt": null,
    "workflow": {
      "id": "3037d010-8e72-4ec4-b905-8157df322055",
      "name": "Order fulfillment"
    },
    "versionId": "ed5d9aa2-f318-4f7f-a597-fb0a530f097f",
    "url": "https://app.processcart.com/runs/3245e6a5-3fbe-463a-ad0b-a7fd871957b0",
    "tasks": [
      {
        "key": "details",
        "title": "Details",
        "kind": "standard",
        "section": "Intake",
        "state": "todo",
        "visible": true,
        "locked": false,
        "dueAt": null,
        "assignees": [],
        "completedAt": null,
        "completedByUserId": null
      },
      {
        "key": "review",
        "title": "Review",
        "kind": "approval",
        "section": "Intake",
        "state": "todo",
        "visible": true,
        "locked": false,
        "dueAt": null,
        "assignees": [
          {
            "kind": "user",
            "userId": "f3e7a668-7e78-44f2-bc92-4457a9a54337"
          }
        ],
        "completedAt": null,
        "completedByUserId": null
      },
      {
        "key": "stop",
        "title": "Sign-off",
        "kind": "standard",
        "section": "Intake",
        "state": "todo",
        "visible": true,
        "locked": false,
        "dueAt": null,
        "assignees": [],
        "completedAt": null,
        "completedByUserId": null
      },
      {
        "key": "wrapup",
        "title": "Wrap up",
        "kind": "standard",
        "section": "Intake",
        "state": "todo",
        "visible": true,
        "locked": true,
        "dueAt": null,
        "assignees": [],
        "completedAt": null,
        "completedByUserId": null
      }
    ],
    "fields": [
      {
        "key": "customer",
        "label": "Customer",
        "type": "text",
        "format": "none",
        "taskKey": "details",
        "value": "Ada",
        "formatted": "Ada",
        "visible": true
      },
      {
        "key": "order_code",
        "label": "Order code",
        "type": "text",
        "format": "custom",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "memo",
        "label": "Memo",
        "type": "textarea",
        "format": null,
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "quantity",
        "label": "Quantity",
        "type": "number",
        "format": "number",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "amount",
        "label": "Amount",
        "type": "number",
        "format": "currency",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "fee",
        "label": "Fee",
        "type": "number",
        "format": "currency",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "rate",
        "label": "Rate",
        "type": "number",
        "format": "percent",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "rate_text",
        "label": "Rate (text)",
        "type": "text",
        "format": "percent",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "ship_date",
        "label": "Ship date",
        "type": "date",
        "format": "date",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "start_time",
        "label": "Start time",
        "type": "time",
        "format": "time",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "gift_wrap",
        "label": "Gift wrap",
        "type": "dropdown",
        "format": null,
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true,
        "choices": [
          "Yes",
          "No"
        ]
      },
      {
        "key": "tags",
        "label": "Tags",
        "type": "multi_choice",
        "format": null,
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true,
        "choices": [
          "A",
          "B",
          "C"
        ]
      },
      {
        "key": "agree",
        "label": "Agree",
        "type": "checkbox",
        "format": null,
        "taskKey": "details",
        "value": false,
        "formatted": "No",
        "visible": true
      },
      {
        "key": "contact_email",
        "label": "Contact email",
        "type": "email",
        "format": "email",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "website",
        "label": "Website",
        "type": "url",
        "format": "url",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "phone",
        "label": "Phone",
        "type": "phone",
        "format": "phone",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "owner",
        "label": "Owner",
        "type": "members",
        "format": null,
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "steps",
        "label": "Steps",
        "type": "subtasks",
        "format": null,
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true,
        "items": [
          {
            "key": "photos",
            "label": "Photos"
          },
          {
            "key": "invoice",
            "label": "Invoice"
          }
        ]
      },
      {
        "key": "contract",
        "label": "Contract",
        "type": "file",
        "format": null,
        "taskKey": "details",
        "value": [
          "03f9307d-039e-484d-8da4-a460186ede29"
        ],
        "formatted": "contract.pdf",
        "visible": true
      },
      {
        "key": "double_amount",
        "label": "Double amount",
        "type": "formula",
        "format": "currency",
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": true
      },
      {
        "key": "source",
        "label": "Source",
        "type": "hidden",
        "format": null,
        "taskKey": "details",
        "value": null,
        "formatted": "",
        "visible": false
      }
    ]
  }
}
```


## GET /v1/runs/{id}/files
**List a run's files.** Scope `runs:read` · checked against the key's workflow / folder allowlist (403 `not_allowed_for_key` outside it). Operation id `listRunFiles` · [open in the API reference](https://developers.processcart.com/reference#tag/files/GET/v1/runs/{id}/files).

Every attachment on the run (File-field files and comment files), oldest first, without download links — `GET /files/{attachmentId}` mints one. A bounded list: `nextCursor` is null, and it takes no query parameters (an unknown one is a 422). A run in Trash still answers.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| 200 | OK. | [ListRunFilesResponse](https://developers.processcart.com/reference/schemas#listrunfilesresponse) |
| 401 | No usable credential. `unauthorized` — Authentication required; `invalid_api_key` — Invalid API key; `key_revoked` — API key revoked. | [Problem](https://developers.processcart.com/errors) |
| 403 | `insufficient_scope` — Insufficient scope; `not_allowed_for_key` — Not allowed for this key. | [Problem](https://developers.processcart.com/errors) |
| 404 | `not_found` — Not found. | [Problem](https://developers.processcart.com/errors) |
| 405 | The verb is not supported on this path. `method_not_allowed` — Method not allowed. | [Problem](https://developers.processcart.com/errors) |
| 422 | `validation_failed` — Validation failed. | [Problem](https://developers.processcart.com/errors) |
| 429 | `rate_limited` — Too many requests. | [Problem](https://developers.processcart.com/errors) |
| 500 | `internal_error` — Internal error. | [Problem](https://developers.processcart.com/errors) |
| 503 | `unavailable` — Service unavailable. | [Problem](https://developers.processcart.com/errors) |

**Errors** (every code this operation can answer; see [Errors](https://developers.processcart.com/errors))

- 401 [`invalid_api_key`](https://developers.processcart.com/errors#invalid_api_key) — Invalid API key
- 401 [`key_revoked`](https://developers.processcart.com/errors#key_revoked) — API key revoked
- 401 [`unauthorized`](https://developers.processcart.com/errors#unauthorized) — Authentication required
- 403 [`insufficient_scope`](https://developers.processcart.com/errors#insufficient_scope) — Insufficient scope
- 403 [`not_allowed_for_key`](https://developers.processcart.com/errors#not_allowed_for_key) — Not allowed for this key
- 404 [`not_found`](https://developers.processcart.com/errors#not_found) — Not found
- 405 [`method_not_allowed`](https://developers.processcart.com/errors#method_not_allowed) — Method not allowed
- 422 [`validation_failed`](https://developers.processcart.com/errors#validation_failed) — Validation failed
- 429 [`rate_limited`](https://developers.processcart.com/errors#rate_limited) — Too many requests
- 500 [`internal_error`](https://developers.processcart.com/errors#internal_error) — Internal error
- 503 [`unavailable`](https://developers.processcart.com/errors#unavailable) — Service unavailable

**Example response** (200)

```json
{
  "data": [
    {
      "id": "03f9307d-039e-484d-8da4-a460186ede29",
      "runId": "3245e6a5-3fbe-463a-ad0b-a7fd871957b0",
      "taskKey": "details",
      "fieldKey": "contract",
      "commentId": null,
      "filename": "contract.pdf",
      "size": 1234,
      "contentType": "application/pdf",
      "uploadedByUserId": null,
      "uploadedBy": {
        "kind": "api",
        "keyId": "d422b49f-a3f2-4244-b97e-8d50f25c3036",
        "name": "collab key"
      },
      "createdAt": "2026-10-13T01:09:44.456Z"
    },
    {
      "id": "58032013-39ef-41cf-a23a-9cde04f17fd8",
      "runId": "3245e6a5-3fbe-463a-ad0b-a7fd871957b0",
      "taskKey": "details",
      "fieldKey": null,
      "commentId": "32629d31-996c-48af-8637-261a6aea7eca",
      "filename": "note.txt",
      "size": 3,
      "contentType": "text/plain",
      "uploadedByUserId": "f3e7a668-7e78-44f2-bc92-4457a9a54337",
      "uploadedBy": {
        "kind": "user",
        "userId": "f3e7a668-7e78-44f2-bc92-4457a9a54337"
      },
      "createdAt": "2026-10-13T01:09:46.727Z"
    }
  ],
  "nextCursor": null,
  "hasMore": false
}
```


## GET /v1/runs/{id}/files/{attachmentId}
**Get a file with a download link.** Scope `runs:read` · checked against the key's workflow / folder allowlist (403 `not_allowed_for_key` outside it). Operation id `getRunFile` · [open in the API reference](https://developers.processcart.com/reference#tag/files/GET/v1/runs/{id}/files/{attachmentId}).

The attachment plus a signed `downloadUrl` (one hour, `Content-Disposition: attachment`). Mint a fresh one per download rather than storing it. A run in Trash still answers.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `attachmentId` | string (uuid) | yes |  |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| 200 | OK. | [Attachment](https://developers.processcart.com/reference/schemas#attachment) |
| 401 | No usable credential. `unauthorized` — Authentication required; `invalid_api_key` — Invalid API key; `key_revoked` — API key revoked. | [Problem](https://developers.processcart.com/errors) |
| 403 | `insufficient_scope` — Insufficient scope; `not_allowed_for_key` — Not allowed for this key. | [Problem](https://developers.processcart.com/errors) |
| 404 | `not_found` — Not found. | [Problem](https://developers.processcart.com/errors) |
| 405 | The verb is not supported on this path. `method_not_allowed` — Method not allowed. | [Problem](https://developers.processcart.com/errors) |
| 422 | `validation_failed` — Validation failed. | [Problem](https://developers.processcart.com/errors) |
| 429 | `rate_limited` — Too many requests. | [Problem](https://developers.processcart.com/errors) |
| 500 | `internal_error` — Internal error. | [Problem](https://developers.processcart.com/errors) |
| 503 | `unavailable` — Service unavailable. | [Problem](https://developers.processcart.com/errors) |

**Errors** (every code this operation can answer; see [Errors](https://developers.processcart.com/errors))

- 401 [`invalid_api_key`](https://developers.processcart.com/errors#invalid_api_key) — Invalid API key
- 401 [`key_revoked`](https://developers.processcart.com/errors#key_revoked) — API key revoked
- 401 [`unauthorized`](https://developers.processcart.com/errors#unauthorized) — Authentication required
- 403 [`insufficient_scope`](https://developers.processcart.com/errors#insufficient_scope) — Insufficient scope
- 403 [`not_allowed_for_key`](https://developers.processcart.com/errors#not_allowed_for_key) — Not allowed for this key
- 404 [`not_found`](https://developers.processcart.com/errors#not_found) — Not found
- 405 [`method_not_allowed`](https://developers.processcart.com/errors#method_not_allowed) — Method not allowed
- 422 [`validation_failed`](https://developers.processcart.com/errors#validation_failed) — Validation failed
- 429 [`rate_limited`](https://developers.processcart.com/errors#rate_limited) — Too many requests
- 500 [`internal_error`](https://developers.processcart.com/errors#internal_error) — Internal error
- 503 [`unavailable`](https://developers.processcart.com/errors#unavailable) — Service unavailable

**Example response** (200)

```json
{
  "id": "03f9307d-039e-484d-8da4-a460186ede29",
  "runId": "3245e6a5-3fbe-463a-ad0b-a7fd871957b0",
  "taskKey": "details",
  "fieldKey": "contract",
  "commentId": null,
  "filename": "contract.pdf",
  "size": 1234,
  "contentType": "application/pdf",
  "uploadedByUserId": null,
  "uploadedBy": {
    "kind": "api",
    "keyId": "d422b49f-a3f2-4244-b97e-8d50f25c3036",
    "name": "collab key"
  },
  "createdAt": "2026-10-13T01:09:44.456Z",
  "downloadUrl": "https://storage.test/storage/v1/object/sign/attachments/5fc61268-81a7-4437-8ef0-05484e4084cf/3245e6a5-3fbe-463a-ad0b-a7fd871957b0/03f9307d-039e-484d-8da4-a460186ede29/contract.pdf?token=dl&download=contract.pdf",
  "downloadExpiresAt": "2026-10-13T02:09:47.501Z"
}
```


## DELETE /v1/runs/{id}/files/{attachmentId}
**Remove a file.** Scope `runs:write` · checked against the key's workflow / folder allowlist (403 `not_allowed_for_key` outside it) · accepts `Idempotency-Key` · accepts `Expected-Revision` (409 `revision_mismatch` when stale). Operation id `deleteRunFile` · [open in the API reference](https://developers.processcart.com/reference#tag/files/DELETE/v1/runs/{id}/files/{attachmentId}).

Removes a File-field file: the field's value drops it under the run lock (the task's required-field gate reacts like any other field change) and the stored object is deleted. Answers 204 with no body — read the run for its new `revision` before the next conditional write; `Expected-Revision` is honored. A comment's file cannot be removed through the API (403); a field of a completed task must be reopened first.

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string (uuid) | yes |  |
| `attachmentId` | string (uuid) | yes |  |

**Request headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | no | Makes the request safe to retry: a second request with the same key and a byte-identical body (and the same `Expected-Revision`) replays the stored response (`Idempotent-Replayed: true`); a different body with the same key is refused (422 `idempotency_key_reused`) and no second resource is created. Keys are stored per workspace for 24 hours and replayed only to the API key that made the request (or the key one rotation away from it); another API key sending the same value is refused (422 `idempotency_key_reused`), so prefix keys per integration. 409, 429 and 5xx answers are never stored. |
| `Expected-Revision` | string | no | Optimistic concurrency: the run's `revision` you last saw (from any Run body) — a bare integer (`7`) or the same in double quotes (`"7"`); `*` or no header = no precondition. Checked INSIDE the run lock — a stale value is refused with 409 `revision_mismatch` whose body names `currentRevision`. A list or any other value is a 422. A successful write answers its new `revision` in the body. The API sends no `ETag` and refuses `If-Match` (422): CDN layers evaluate `If-Match` against the response and turn a successful write into a 412. |

**Responses**

| Status | Meaning | Body |
| --- | --- | --- |
| 204 | Done; no body. | — |
| 401 | No usable credential. `unauthorized` — Authentication required; `invalid_api_key` — Invalid API key; `key_revoked` — API key revoked. | [Problem](https://developers.processcart.com/errors) |
| 403 | `insufficient_scope` — Insufficient scope; `not_allowed_for_key` — Not allowed for this key; `read_only_key` — Read-only key. | [Problem](https://developers.processcart.com/errors) |
| 404 | `not_found` — Not found. | [Problem](https://developers.processcart.com/errors) |
| 405 | The verb is not supported on this path. `method_not_allowed` — Method not allowed. | [Problem](https://developers.processcart.com/errors) |
| 409 | `idempotency_in_progress` — Request in progress; `revision_mismatch` — Revision mismatch; `run_archived` — Run is archived; `run_trashed` — Run is in Trash; `task_completed` — Task already completed. | [Problem](https://developers.processcart.com/errors) |
| 422 | `idempotency_key_reused` — Idempotency key reused; `validation_failed` — Validation failed. | [Problem](https://developers.processcart.com/errors) |
| 429 | `rate_limited` — Too many requests. | [Problem](https://developers.processcart.com/errors) |
| 500 | `internal_error` — Internal error. | [Problem](https://developers.processcart.com/errors) |
| 503 | `unavailable` — Service unavailable. | [Problem](https://developers.processcart.com/errors) |

**Errors** (every code this operation can answer; see [Errors](https://developers.processcart.com/errors))

- 401 [`invalid_api_key`](https://developers.processcart.com/errors#invalid_api_key) — Invalid API key
- 401 [`key_revoked`](https://developers.processcart.com/errors#key_revoked) — API key revoked
- 401 [`unauthorized`](https://developers.processcart.com/errors#unauthorized) — Authentication required
- 403 [`insufficient_scope`](https://developers.processcart.com/errors#insufficient_scope) — Insufficient scope
- 403 [`not_allowed_for_key`](https://developers.processcart.com/errors#not_allowed_for_key) — Not allowed for this key
- 403 [`read_only_key`](https://developers.processcart.com/errors#read_only_key) — Read-only key
- 404 [`not_found`](https://developers.processcart.com/errors#not_found) — Not found
- 405 [`method_not_allowed`](https://developers.processcart.com/errors#method_not_allowed) — Method not allowed
- 409 [`idempotency_in_progress`](https://developers.processcart.com/errors#idempotency_in_progress) — Request in progress
- 409 [`revision_mismatch`](https://developers.processcart.com/errors#revision_mismatch) — Revision mismatch
- 409 [`run_archived`](https://developers.processcart.com/errors#run_archived) — Run is archived
- 409 [`run_trashed`](https://developers.processcart.com/errors#run_trashed) — Run is in Trash
- 409 [`task_completed`](https://developers.processcart.com/errors#task_completed) — Task already completed
- 422 [`idempotency_key_reused`](https://developers.processcart.com/errors#idempotency_key_reused) — Idempotency key reused
- 422 [`validation_failed`](https://developers.processcart.com/errors#validation_failed) — Validation failed
- 429 [`rate_limited`](https://developers.processcart.com/errors#rate_limited) — Too many requests
- 500 [`internal_error`](https://developers.processcart.com/errors#internal_error) — Internal error
- 503 [`unavailable`](https://developers.processcart.com/errors#unavailable) — Service unavailable
