Files endpoints

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

View this page as Markdown

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 and the interactive reference.

Operation Method and path Scope
Request an upload ticket POST /v1/runs/{id}/files Scope runs:write
Finalize an upload POST /v1/runs/{id}/files/{attachmentId}/finalize Scope runs:write
List a run's files GET /v1/runs/{id}/files Scope runs:read
Get a file with a download link GET /v1/runs/{id}/files/{attachmentId} Scope runs:read
Remove a file DELETE /v1/runs/{id}/files/{attachmentId} 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.

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

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
401 No usable credential. unauthorized — Authentication required; invalid_api_key — Invalid API key; key_revoked — API key revoked. Problem
403 insufficient_scope — Insufficient scope; not_allowed_for_key — Not allowed for this key; read_only_key — Read-only key. Problem
404 not_found — Not found. Problem
405 The verb is not supported on this path. method_not_allowed — Method not allowed. Problem
409 idempotency_in_progress — Request in progress; run_archived — Run is archived; run_trashed — Run is in Trash. Problem
413 payload_too_large — Payload too large. Problem
415 unsupported_media_type — Unsupported media type. Problem
422 field_not_writable — Field is not writable; idempotency_key_reused — Idempotency key reused; validation_failed — Validation failed. Problem
429 rate_limited — Too many requests. Problem
500 internal_error — Internal error. Problem
503 unavailable — Service unavailable. Problem

Errors (every code this operation can answer; see Errors)

Example request

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

Example response (200)

{
  "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.

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

Property Type Required Description
filename string yes
target object yes

Responses

Status Meaning Body
200 OK. FinalizeRunFileResponse
401 No usable credential. unauthorized — Authentication required; invalid_api_key — Invalid API key; key_revoked — API key revoked. Problem
403 insufficient_scope — Insufficient scope; not_allowed_for_key — Not allowed for this key; read_only_key — Read-only key. Problem
404 not_found — Not found. Problem
405 The verb is not supported on this path. method_not_allowed — Method not allowed. Problem
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
413 payload_too_large — Payload too large. Problem
415 unsupported_media_type — Unsupported media type. Problem
422 field_not_writable — Field is not writable; idempotency_key_reused — Idempotency key reused; validation_failed — Validation failed. Problem
429 rate_limited — Too many requests. Problem
500 internal_error — Internal error. Problem
503 unavailable — Service unavailable. Problem

Errors (every code this operation can answer; see Errors)

Example request

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

Example response (200)

{
  "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.

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
401 No usable credential. unauthorized — Authentication required; invalid_api_key — Invalid API key; key_revoked — API key revoked. Problem
403 insufficient_scope — Insufficient scope; not_allowed_for_key — Not allowed for this key. Problem
404 not_found — Not found. Problem
405 The verb is not supported on this path. method_not_allowed — Method not allowed. Problem
422 validation_failed — Validation failed. Problem
429 rate_limited — Too many requests. Problem
500 internal_error — Internal error. Problem
503 unavailable — Service unavailable. Problem

Errors (every code this operation can answer; see Errors)

Example response (200)

{
  "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.

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
401 No usable credential. unauthorized — Authentication required; invalid_api_key — Invalid API key; key_revoked — API key revoked. Problem
403 insufficient_scope — Insufficient scope; not_allowed_for_key — Not allowed for this key. Problem
404 not_found — Not found. Problem
405 The verb is not supported on this path. method_not_allowed — Method not allowed. Problem
422 validation_failed — Validation failed. Problem
429 rate_limited — Too many requests. Problem
500 internal_error — Internal error. Problem
503 unavailable — Service unavailable. Problem

Errors (every code this operation can answer; see Errors)

Example response (200)

{
  "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.

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
403 insufficient_scope — Insufficient scope; not_allowed_for_key — Not allowed for this key; read_only_key — Read-only key. Problem
404 not_found — Not found. Problem
405 The verb is not supported on this path. method_not_allowed — Method not allowed. Problem
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
422 idempotency_key_reused — Idempotency key reused; validation_failed — Validation failed. Problem
429 rate_limited — Too many requests. Problem
500 internal_error — Internal error. Problem
503 unavailable — Service unavailable. Problem

Errors (every code this operation can answer; see Errors)