> For the complete documentation index, see [llms.txt](https://docs.annolab.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.annolab.ai/projects-directories-and-sources/upload-sources.md).

# Upload Sources

Upload PDF source files using a three-step process: initialize an upload, PUT the file to a signed URL, then confirm creation and start processing.

PDF uploads use a three-step process. This is the recommended way to add PDFs through the API.

1. **Initialize** the upload with `POST /v1/source/init-pdf`. This creates a pending source and returns a signed `uploadUrl`.
2. **Upload** the PDF with `PUT` to that `uploadUrl`. Do not send your API key on this request.
3. **Confirm** the upload with `POST /v1/source/create-pdf`. This starts OCR and, optionally, a workflow of AI models.

Complete all three steps before the pending source `expiresAt` time. Creating sources requires an API key with "Write" permissions.

## 1. Initialize PDF upload

<mark style="color:green;">`POST`</mark> `https://api.annolab.ai/v1/source/init-pdf`

Create a pending PDF source and receive a signed URL to upload the file.

#### Headers

| Name          | Type   | Description                                                                                                                                                             |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Authorization | string | <p>Where you put your api key. Initializing an upload requires a key with "Write" permissions.<br><code>{"Authorization": "Api-Key XXXXXXX-XXXXXXX-XXXXXXX"}</code></p> |

#### Request Body

| Name                | Type            | Description                                                                                                                                                              |
| ------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| projectIdentifier   | string\|integer | Id or unique name of the project that will contain the source                                                                                                            |
| sourceName          | string          | Name of the source to create                                                                                                                                             |
| directoryIdentifier | string\|integer | Optional. Id or name of the directory. If omitted, the project's default Uploads directory is used. If a name is passed and the directory does not exist, it is created. |
| groupName           | string          | Optional. Group that owns the project. When provided, the project is resolved as that group's project named or identified by `projectIdentifier`.                        |
| tags                | CanonicalTag\[] | Optional. Array of [CanonicalTag](/annotations-and-relations/canonical-tags.md#canonicaltag-object) objects to attach to the source                                      |
| metadata            | object          | Optional. Key-value metadata to store with the source                                                                                                                    |
| abstractId          | integer         | Optional. Abstract to associate with this source                                                                                                                         |

{% tabs %}
{% tab title="201 The pending source was created" %}

```json
{
    "projectName": "Example Land Project",
    "projectId": 22,
    "directoryName": "Uploads",
    "directoryId": 12,
    "sourceName": "deed.pdf",
    "uploadUrl": "https://s3.amazonaws.com/example-bucket/path/to/deed.pdf?X-Amz-Signature=...",
    "expiresAt": "2026-08-20T19:00:00.000Z",
    "abstractId": 1842,
    "pendingSource": {
        "pendingSourceId": 501,
        "name": "deed.pdf",
        "sourceType": "pdf",
        "createdAt": "2026-08-20T18:00:00.000Z",
        "expiresAt": "2026-08-20T19:00:00.000Z",
        "confirmedAt": "2026-08-20T19:00:00.000Z",
        "metadata": null,
        "status": null,
        "finalSourceId": null
    }
}
```

{% endtab %}

{% tab title="400 The pending source could not be created" %}

```json
{
    "message": "Project Example Land Project does not exist"
}
```

{% endtab %}
{% endtabs %}

Save `uploadUrl` for step 2 and `pendingSource.pendingSourceId` (or `sourceName`) for step 3.

{% tabs %}
{% tab title="Python" %}

```python
import requests

ANNO_LAB_API_KEY = 'XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX'

headers = {
  'Authorization': 'Api-Key '+ANNO_LAB_API_KEY,
}

init_body = {
  'projectIdentifier': 'Example Land Project',
  'directoryIdentifier': 'Uploads',
  'sourceName': 'deed.pdf'
}

init_response = requests.post(
  'https://api.annolab.ai/v1/source/init-pdf',
  headers=headers,
  json=init_body
)

init_json = init_response.json()
upload_url = init_json['uploadUrl']
source_name = init_json['sourceName']
```

{% endtab %}
{% endtabs %}

## 2. Upload the PDF

<mark style="color:orange;">`PUT`</mark> `{uploadUrl}`

Upload the PDF bytes to the signed URL returned by `init-pdf`. This request goes to object storage, not to `api.annolab.ai`.

Do not include the `Authorization` API key header. Send the raw file contents as the request body.

{% tabs %}
{% tab title="Python" %}

```python
pdf_path = '/path/to/deed.pdf'

with open(pdf_path, 'rb') as pdf_file:
  upload_response = requests.put(upload_url, data=pdf_file)

upload_response.raise_for_status()
```

{% endtab %}
{% endtabs %}

A successful upload typically returns `200`. After the file is in place, confirm the source in step 3.

## 3. Create the PDF source

<mark style="color:green;">`POST`</mark> `https://api.annolab.ai/v1/source/create-pdf`

Confirm that the PDF has been uploaded and start processing. This creates the source from the pending upload and can run OCR and a workflow of AI models.

You must have permission to create sources in the project. `sourceIdentifier` must match the pending source from step 1, either by name (`sourceName`) or by `pendingSourceId`.

#### Headers

| Name          | Type   | Description                                                                                                                                                        |
| ------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Authorization | string | <p>Where you put your api key. Creating a source requires a key with "Write" permissions.<br><code>{"Authorization": "Api-Key XXXXXXX-XXXXXXX-XXXXXXX"}</code></p> |

#### Request Body

| Name                | Type            | Description                                                                                                                                                                 |
| ------------------- | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| projectIdentifier   | string\|integer | Id or unique name of the project that contains the pending source                                                                                                           |
| sourceIdentifier    | string\|integer | Pending source name from `init-pdf`, or the `pendingSourceId`                                                                                                               |
| directoryIdentifier | string\|integer | Optional. Id or name of the directory used in `init-pdf`. If omitted, the project's default Uploads directory is used.                                                      |
| groupName           | string          | Optional. Group that owns the project. When provided, the project is resolved as that group's project named or identified by `projectIdentifier`.                           |
| ocr                 | boolean         | Optional. Defaults to running OCR. Set to `false` to extract existing PDF text instead of OCR.                                                                              |
| ocrProvider         | string          | Optional. OCR engine when OCR is enabled. One of `textract`, `textract_plus`, or `gcv`. `textract_plus` is recommended for highest quality.                                 |
| preprocessor        | string          | Optional. Document preprocessor. One of `faa` or `none`.                                                                                                                    |
| workflow            | string          | Optional. Name of the workflow (blueprint) to run after upload. The returned `executionId` can be queried with [Get Workflow Execution](/workflows/workflow-executions.md). |
| abstractId          | integer         | Optional. Abstract to associate with this source                                                                                                                            |

{% tabs %}
{% tab title="201 The source was created" %}

```json
{
    "projectName": "Example Land Project",
    "projectId": 22,
    "directoryName": "Uploads",
    "directoryId": 12,
    "sourceName": "deed.pdf",
    "executionId": 1842,
    "uploadUrl": "https://s3.amazonaws.com/example-bucket/path/to/deed.pdf?X-Amz-Signature=...",
    "pendingSource": {
        "pendingSourceId": 501,
        "name": "deed.pdf",
        "sourceType": "pdf",
        "uploadUrl": "https://s3.amazonaws.com/example-bucket/path/to/deed.pdf?X-Amz-Signature=...",
        "createdAt": "2026-08-20T18:00:00.000Z",
        "expiresAt": "2026-08-20T19:00:00.000Z",
        "confirmedAt": "2026-08-20T19:00:00.000Z",
        "metadata": null,
        "status": "Waiting",
        "finalSourceId": null
    }
}
```

{% endtab %}

{% tab title="403 Forbidden" %}

```json
{
    "message": "You are not permitted to create sources for this project"
}
```

{% endtab %}

{% tab title="404 The pending source was not found" %}

```json
{
    "message": "There is no pending pdf with that name, did you initialize pdf source creation with POST source/init-pdf?"
}
```

{% endtab %}

{% tab title="409 The source was already created" %}

```json
{
    "message": "A pdf source has already been created. If you feel this message is inaccurate, make sure to initialize pdf source creation with source/init-pdf. "
}
```

{% endtab %}
{% endtabs %}

`create-pdf` returns a pending source. Processing continues asynchronously. If a workflow was requested, `executionId` is the id of that [workflow execution](/workflows/workflow-executions.md). `pendingSource.finalSourceId` is populated once processing finishes; use that id with the [Source Files](/projects-directories-and-sources/source-files.md) API.

{% tabs %}
{% tab title="Python" %}

```python
create_body = {
  'projectIdentifier': 'Example Land Project',
  'directoryIdentifier': 'Uploads',
  'sourceIdentifier': source_name,
  'ocr': True,
  'ocrProvider': 'textract_plus',
  'workflow': 'land_title'
}

create_response = requests.post(
  'https://api.annolab.ai/v1/source/create-pdf',
  headers=headers,
  json=create_body
)

print(create_response.json())
```

{% endtab %}
{% endtabs %}

## Full example

{% tabs %}
{% tab title="Python" %}

```python
import requests

ANNO_LAB_API_KEY = 'XXXXXXX-XXXXXXX-XXXXXXX-XXXXXXX'
pdf_path = '/path/to/deed.pdf'

headers = {
  'Authorization': 'Api-Key '+ANNO_LAB_API_KEY,
}

# 1. Initialize the pending source and get a signed upload URL
init_response = requests.post(
  'https://api.annolab.ai/v1/source/init-pdf',
  headers=headers,
  json={
    'projectIdentifier': 'Example Land Project',
    'directoryIdentifier': 'Uploads',
    'sourceName': 'deed.pdf'
  }
)
init_json = init_response.json()
upload_url = init_json['uploadUrl']
source_name = init_json['sourceName']

# 2. PUT the PDF to the signed URL (no API key)
with open(pdf_path, 'rb') as pdf_file:
  upload_response = requests.put(upload_url, data=pdf_file)
upload_response.raise_for_status()

# 3. Confirm the upload and start OCR / workflow
create_response = requests.post(
  'https://api.annolab.ai/v1/source/create-pdf',
  headers=headers,
  json={
    'projectIdentifier': 'Example Land Project',
    'directoryIdentifier': 'Uploads',
    'sourceIdentifier': source_name,
    'ocr': True,
    'ocrProvider': 'textract_plus',
    'workflow': 'land_title'
  }
)

print(create_response.json())
```

{% endtab %}
{% endtabs %}
