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

# Uploading your own document

> Circulate a PDF you already have

A document does not have to come from a Doodocs template. When an external
system has already produced the PDF — an order from 1C, a contract from a CRM,
a scan — upload it and route it like any other document.

The bytes travel outside the API: the server hands you a presigned form, you
send the file straight to storage, confirm the upload, then create the document
by `file_id`. All three steps are covered by the `documents:write` scope.

<Frame caption="From a file to a document in flight">
  ```mermaid theme={null}
  %%{init: {'theme':'neutral'}}%%
  sequenceDiagram
      participant C as Your code
      participant A as Doodocs People API
      participant S as Storage
      C->>A: POST /files (name, type, size)
      A-->>C: file.id + presigned form
      C->>S: POST the form with the bytes
      S-->>C: 204
      C->>A: POST /files/{id}/_confirm
      A-->>C: status UPLOADED
      C->>A: POST /documents (file_id)
      A-->>C: document, status DRAFT
      C->>A: PUT /documents/{id}/route → _send
  ```
</Frame>

## Step 1. Reserve the file

`POST /developer/v1/files` creates the record and returns the upload form.

```bash theme={null}
curl -X POST -H "X-API-Key: ddp_YOUR_KEY" -H "Content-Type: application/json" \
  https://app.doodocs.kz/api/developer/v1/files \
  -d '{
    "filename": "employment-order.pdf",
    "content_type": "application/pdf",
    "size_bytes": 148392
  }'
```

```json theme={null}
{
  "file": {
    "id": "01a0235f-697e-7ce1-ba86-bcde95928a68",
    "filename": "employment-order.pdf",
    "content_type": "application/pdf",
    "size_bytes": 148392,
    "status": "FILE_UPLOAD_STATUS_PENDING",
    "create_time": "2026-08-21T09:30:00Z"
  },
  "upload": {
    "url": "https://storage.doodocs.kz/people-files",
    "fields": {
      "key": "…/documents/01a0235f…",
      "Content-Type": "application/pdf",
      "policy": "eyJleHBpcmF0aW9uIjoi…",
      "x-amz-signature": "9f2c…"
    },
    "expires_at": "2026-08-21T09:45:00Z"
  }
}
```

<Note>
  `size_bytes` is the exact byte count. The signed policy rejects an object larger
  than that, and `_confirm` compares what storage holds against what you declared.
</Note>

## Step 2. Send the bytes to storage

Submit the form as `multipart/form-data`: every entry of `fields` verbatim
first, the file last as the `file` part.

```bash theme={null}
curl -X POST "https://storage.doodocs.kz/people-files" \
  -F "key=…/documents/01a0235f…" \
  -F "Content-Type=application/pdf" \
  -F "policy=eyJleHBpcmF0aW9uIjoi…" \
  -F "x-amz-signature=9f2c…" \
  -F "file=@employment-order.pdf"
```

A successful upload is `204` with no body. Do not send `X-API-Key` here — the
signature in the form already authorizes the request. The form is valid until
`expires_at`; after that, reserve the file again.

## Step 3. Confirm the upload

Until the upload is confirmed, the file cannot be attached to a document.

```bash theme={null}
curl -X POST -H "X-API-Key: ddp_YOUR_KEY" -H "Content-Type: application/json" \
  https://app.doodocs.kz/api/developer/v1/files/01a0235f-697e-7ce1-ba86-bcde95928a68/_confirm \
  -d '{}'
```

```json theme={null}
{
  "file": {
    "id": "01a0235f-697e-7ce1-ba86-bcde95928a68",
    "status": "FILE_UPLOAD_STATUS_UPLOADED",
    "size_bytes": 148392
  }
}
```

Confirming an already confirmed file changes nothing and returns `200`, so
retrying after a timeout is safe.

## Step 4. Create the document from the file

Pass `file_id` to `POST /documents` instead of `template_id`. The file becomes
the document's PDF — exactly what approvers see and signers sign.

```bash theme={null}
curl -X POST -H "X-API-Key: ddp_YOUR_KEY" -H "Content-Type: application/json" \
  https://app.doodocs.kz/api/developer/v1/documents \
  -d '{
    "title": "Employment order — Qanysh Satbayev",
    "type_id": "t1…",
    "file_id": "01a0235f-697e-7ce1-ba86-bcde95928a68",
    "subject_employee_ids": ["3f2a9c7e-…"]
  }'
```

<Warning>
  `template_id` and `file_id` are mutually exclusive: passing both or neither is
  `DEVELOPER.INVALID_ARGUMENT`. `template_values` without `template_id` is
  rejected too, rather than silently ignored.
</Warning>

From here the path is the usual one — [route, send and
sign](/en/guides/documents/signing). The key's owner becomes the initiator, the
same as when creating from a template.

## Limits

| What           | Value                                             |
| -------------- | ------------------------------------------------- |
| Format         | `application/pdf` only                            |
| Size           | up to 50 MB                                       |
| Unconfirmed    | at most 10 at a time per key owner                |
| One attachment | a confirmed file attaches to exactly one document |

<Note>
  Nothing is converted: a DOCX or an image is rejected, and the file goes to
  signing exactly as you sent it. Produce the PDF on your side.
</Note>

The unconfirmed-upload limit counts by key owner and is shared with the web app:
an integration that reserves files without confirming them also consumes that
person's quota in the interface.

## Errors

| `code`                           | When it happens                                                    |
| -------------------------------- | ------------------------------------------------------------------ |
| `FILE.INVALID_CONTENT_TYPE`      | `content_type` is not `application/pdf`                            |
| `FILE.INVALID_SIZE`              | `size_bytes` is zero, negative, or above the limit                 |
| `FILE.PENDING_LIMIT_EXCEEDED`    | Too many unconfirmed uploads                                       |
| `FILE_NOT_FOUND`                 | No such file, it belongs to another owner, or the id is not a UUID |
| `FILE.NOT_UPLOADED`              | `_confirm` was called but the bytes never reached storage          |
| `FILE.SIZE_MISMATCH`             | The stored object is larger than declared                          |
| `FILE.INVALID_STATUS_TRANSITION` | `file_id` was passed to `POST /documents` before confirmation      |
| `FILE.ALREADY_LINKED`            | The file is already attached to another document                   |

<Note>
  A document created from a file gets no number automatically — numbering is tied
  to templates. Pass `number` in `POST /documents` if you need one.
</Note>
