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

# Upload Files

> Autonomous file uploads via presigned S3 URLs, with explicit upload confirmation

# Upload Files

Use this 3-step handshake to upload files from browsers, scripts, CI jobs, or the Dcycle CLI without streaming file bytes through the API server.

<Steps>
  <Step title="Create presigned upload URLs">
    Call `POST /v1/files/presigned-urls`. The backend creates `pending` file rows and returns one presigned S3 URL per file.
  </Step>

  <Step title="Upload file bytes to S3">
    `PUT` the raw file bytes to each presigned URL using the same `Content-Type` you sent in step 1.
  </Step>

  <Step title="Confirm the upload">
    Call `PATCH /v1/files/batch-update` with `status=uploaded`. This marks the file as uploaded and emits `CLASSIFY_DOCUMENT`.
  </Step>
</Steps>

## Step 1: Create Presigned URLs

### Headers

<ParamField header="x-api-key" type="string" required>
  Your API key for authentication

  **Example:** `sk_live_1234567890abcdef`
</ParamField>

<ParamField header="x-organization-id" type="string" required>
  Your organization UUID

  **Example:** `a8315ef3-dd50-43f8-b7ce-d839e68d51fa`
</ParamField>

### Body

Send an array so one request can prepare multiple uploads.

<ParamField body="name" type="string" required>
  File name without the extension.
</ParamField>

<ParamField body="extension" type="string" required>
  File extension such as `pdf`, `csv`, `xlsx`, `jpg`, or `png`.
</ParamField>

<ParamField body="mime_type" type="string" required>
  MIME type for the uploaded file.
</ParamField>

<ParamField body="size_kb" type="integer" required>
  File size in kilobytes.
</ParamField>

<ParamField body="folder_id" type="string">
  Optional folder UUID. Omit for root-level uploads.
</ParamField>

### Example

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST "https://api.dcycle.io/v1/files/presigned-urls" \
    -H "x-api-key: ${DCYCLE_API_KEY}" \
    -H "x-organization-id: ${DCYCLE_ORG_ID}" \
    -H "Content-Type: application/json" \
    -d '[
      {
        "name": "invoice_january",
        "extension": "pdf",
        "mime_type": "application/pdf",
        "size_kb": 2048
      }
    ]'
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import os
  import requests

  response = requests.post(
      "https://api.dcycle.io/v1/files/presigned-urls",
      headers={
          "x-api-key": os.environ["DCYCLE_API_KEY"],
          "x-organization-id": os.environ["DCYCLE_ORG_ID"],
          "Content-Type": "application/json",
      },
      json=[
          {
              "name": "invoice_january",
              "extension": "pdf",
              "mime_type": "application/pdf",
              "size_kb": 2048,
          }
      ],
      timeout=30,
  )

  file_upload = response.json()[0]
  print(file_upload["id"], file_upload["presigned_url"])
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const axios = require('axios');

  const response = await axios.post('https://api.dcycle.io/v1/files/presigned-urls', [
    {
      name: 'invoice_january',
      extension: 'pdf',
      mime_type: 'application/pdf',
      size_kb: 2048,
    },
  ], {
    headers: {
      'x-api-key': process.env.DCYCLE_API_KEY,
      'x-organization-id': process.env.DCYCLE_ORG_ID,
      'Content-Type': 'application/json',
    },
  });

  const fileUpload = response.data[0];
  console.log(fileUpload.id, fileUpload.presigned_url);
  ```
</CodeGroup>

### Response Fields

<ResponseField name="id" type="string">
  File UUID stored in PostgreSQL.
</ResponseField>

<ResponseField name="status" type="string">
  Starts as `pending`.
</ResponseField>

<ResponseField name="url" type="string">
  Final S3-backed file URL stored on the File record.
</ResponseField>

<ResponseField name="presigned_url" type="string">
  Temporary S3 `PUT` URL used for the direct upload.
</ResponseField>

### Successful Response

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
[
  {
    "id": "11111111-1111-1111-1111-111111111111",
    "name": "invoice_january",
    "extension": "pdf",
    "mime_type": "application/pdf",
    "size_kb": 2048,
    "url": "https://dcycle-files.s3.eu-west-1.amazonaws.com/orgs/a8315ef3/invoice_january.pdf",
    "status": "pending",
    "presigned_url": "https://dcycle-files.s3.eu-west-1.amazonaws.com/orgs/a8315ef3/invoice_january.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&..."
  }
]
```

## Step 2: Upload to S3

Upload the raw file bytes to the `presigned_url` returned in step 1.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X PUT "${PRESIGNED_URL}" \
    -H "Content-Type: application/pdf" \
    --data-binary @invoice_january.pdf
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  with open("invoice_january.pdf", "rb") as file_handle:
      upload_response = requests.put(
          file_upload["presigned_url"],
          headers={"Content-Type": "application/pdf"},
          data=file_handle,
          timeout=300,
      )

  upload_response.raise_for_status()
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const fs = require('fs');
  const axios = require('axios');

  await axios.put(fileUpload.presigned_url, fs.readFileSync('invoice_january.pdf'), {
    headers: { 'Content-Type': 'application/pdf' },
  });
  ```
</CodeGroup>

## Step 3: Confirm the Upload

`PATCH /v1/files/batch-update` is the step that flips the file from `pending` to `uploaded` and triggers document classification.

### Body

<ParamField body="file_ids" type="string[]" required>
  File IDs returned by step 1.
</ParamField>

<ParamField body="status" type="string" required>
  Use `uploaded` after a successful S3 upload, or `error` if the upload failed.
</ParamField>

<ParamField body="project_id" type="string">
  Optional project UUID. When present, the backend also creates `file_project` links.
</ParamField>

### Example

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X PATCH "https://api.dcycle.io/v1/files/batch-update" \
    -H "x-api-key: ${DCYCLE_API_KEY}" \
    -H "x-organization-id: ${DCYCLE_ORG_ID}" \
    -H "Content-Type: application/json" \
    -d '{
      "file_ids": ["11111111-1111-1111-1111-111111111111"],
      "status": "uploaded"
    }'
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  requests.patch(
      "https://api.dcycle.io/v1/files/batch-update",
      headers={
          "x-api-key": os.environ["DCYCLE_API_KEY"],
          "x-organization-id": os.environ["DCYCLE_ORG_ID"],
          "Content-Type": "application/json",
      },
      json={
          "file_ids": [file_upload["id"]],
          "status": "uploaded",
      },
      timeout=30,
  ).raise_for_status()
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  await axios.patch('https://api.dcycle.io/v1/files/batch-update', {
    file_ids: [fileUpload.id],
    status: 'uploaded',
  }, {
    headers: {
      'x-api-key': process.env.DCYCLE_API_KEY,
      'x-organization-id': process.env.DCYCLE_ORG_ID,
      'Content-Type': 'application/json',
    },
  });
  ```
</CodeGroup>

## Event Trigger

When `status=uploaded`, the backend emits `CLASSIFY_DOCUMENT` with:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "resource": "POST",
  "method": "/classify-document",
  "file_id": "file-uuid",
  "user_id": "user-uuid",
  "organization_id": "organization-uuid"
}
```

## Common Errors

### 401 Unauthorized

**Cause:** Missing or invalid API key

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"detail": "Invalid API key for organization", "code": "INVALID_API_KEY"}
```

### 403 Forbidden

**Cause:** The authenticated user is not a member of the organization

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"detail": "Logged User is not Member of Organization", "code": "LOGGED_USER_NOT_MEMBER"}
```

### 422 Unprocessable Entity

**Cause:** Missing required fields (`name`, `extension`, `mime_type`, `size_kb`)

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "detail": [
    {
      "loc": ["body", 0, "name"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}
```

## Legacy Alternative

If you do not need the direct-to-S3 flow, `POST /v1/files/upload` still accepts a standard multipart upload through the backend.

## Related Endpoints

<CardGroup cols={2}>
  <Card title="List File Readings" icon="file-lines" href="/api-reference/files/readings">
    Retrieve extracted readings after processing
  </Card>

  <Card title="Retry Processing" icon="rotate" href="/api-reference/files/process">
    Re-queue a file for processing
  </Card>

  <Card title="Update Reading" icon="pencil" href="/api-reference/files/update-reading">
    Edit extracted content before creating records
  </Card>

  <Card title="Create Records" icon="plus" href="/api-reference/files/create-records">
    Convert readings into invoices or wastes
  </Card>
</CardGroup>
