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

# POST /uploads/create-target

> Returns a presigned S3 POST so you can upload an attachment for use in a generation.

<RequestExample>
  ```shellscript cURL theme={null}
  curl "https://api.chroniclehq.com/api/v1/uploads/create-target" \
    -X POST \
    -H "Authorization: Bearer $API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "file_name": "research-notes.pdf",
      "content_type": "application/pdf",
      "declared_file_size": 524288
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(
    "https://api.chroniclehq.com/api/v1/uploads/create-target",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        file_name: "research-notes.pdf",
        content_type: "application/pdf",
        declared_file_size: 524288,
      }),
    },
  );

  const data = await response.json();
  console.log(data);
  ```
</RequestExample>

### Parameters

<ParamField body="file_name" type="string" required>
  The file's original name, including extension. Used for display and to derive
  the file type downstream.
</ParamField>

<ParamField body="content_type" type="string" required>
  The MIME type of the file (for example `application/pdf`). Must be in the
  allowed list — see the FAQ below.
</ParamField>

<ParamField body="declared_file_size" type="number" required>
  The size of the file in bytes. Capped at 50 MB; the cap is also enforced by S3
  on the upload itself.
</ParamField>

### Response

<ResponseExample>
  ```json Response theme={null}
  {
    "url": "https://chronicle-attachments.s3.us-east-1.amazonaws.com/",
    "fields": [
      { "name": "Content-Type", "value": "application/pdf" },
      {
        "name": "key",
        "value": "file-attachments/ws_123/abc-research-notes.pdf"
      },
      { "name": "Policy", "value": "..." },
      { "name": "X-Amz-Signature", "value": "..." }
    ],
    "path": "file-attachments/ws_123/abc-research-notes.pdf",
    "download_url": "https://chronicle-attachments.s3.us-east-1.amazonaws.com/file-attachments/ws_123/abc-research-notes.pdf?X-Amz-..."
  }
  ```
</ResponseExample>

<ResponseField name="url" type="string">
  The S3 endpoint to `POST` the file to.
</ResponseField>

<ResponseField name="fields" type="object[]">
  Form fields that must be included in the multipart upload, in order, before
  the `file` field. Each entry has `name` and `value`.
</ResponseField>

<ResponseField name="path" type="string">
  The S3 object key the file will be stored under.
</ResponseField>

<ResponseField name="download_url" type="string">
  A signed `GET` URL valid for 7 days. Pass this as the `url` on each attachment
  object when calling [POST
  /presentations/generate](/post-presentations-generate) or [POST
  /presentations/generate/:generationId/message](/post-send-followup-message).
</ResponseField>

***

## Uploading the file

After this endpoint returns, post the file directly to S3 using the returned `url` and `fields`. The `file` field must come last in the form.

<RequestExample>
  ```shellscript cURL theme={null}
  curl "$URL" \
    -F "Content-Type=application/pdf" \
    -F "key=file-attachments/ws_123/abc-research-notes.pdf" \
    -F "Policy=..." \
    -F "X-Amz-Signature=..." \
    -F "file=@research-notes.pdf"
  ```

  ```javascript JavaScript theme={null}
  const form = new FormData();
  for (const f of target.fields) form.append(f.name, f.value);
  form.append("file", file); // must be last

  const upload = await fetch(target.url, {
    method: "POST",
    body: form,
  });
  // upload.status === 204 on success
  ```
</RequestExample>

A successful upload returns `204 No Content`. **Do not** send an `Authorization` header on this request — S3 will reject it.

Once uploaded, reference the file in a generation by passing the captured `download_url`:

```json Attachment object theme={null}
{
  "id": "att_123",
  "url": "<download_url from create-target>",
  "file_name": "research-notes.pdf",
  "type": "application/pdf"
}
```

***

## FAQs

<AccordionGroup>
  <Accordion title="Why two steps instead of uploading directly to Chronicle?">
    The presigned POST sends the file bytes straight to S3, so large attachments don't traverse the Chronicle API. The Chronicle endpoint stays lightweight (just metadata) and the upload itself can stream through S3's regional ingress.
  </Accordion>

  <Accordion title="Which file types are allowed?">
    `.txt`, `.md`, `.pdf`, `.pptx`, and images (`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`).

    Sending `content_type` outside this list returns `FILE_UPLOAD_ERROR`.
  </Accordion>

  {" "}

  <Accordion title="What's the size limit?">
    50 MB per file. Larger files are rejected with `FILE_UPLOAD_ERROR`. The cap is
    enforced both at this endpoint (against `declared_file_size`) and at S3
    (against the actual upload).
  </Accordion>

  {" "}

  <Accordion title="How long is `download_url` valid?">
    7 days. As long as you reference it in a generation within that window, it'll
    work. After expiry you'll need to upload the file again.
  </Accordion>

  <Accordion title="What workspace does the upload belong to?">
    The workspace tied to your API key. Attachments are scoped to that workspace and aren't visible to other workspaces.
  </Accordion>
</AccordionGroup>

***

## Related posts

* [POST /presentations/generate](/post-presentations-generate)
* [POST /presentations/generate/:generationId/message](/post-send-followup-message)
* [API scope and rate limits](/api-scope-rate-limits)
