---
title: "Upload API"
description: "Use when you upload a PDF, EPUB or Markdown file over HTTP: check for duplicates, get an upload URL, send the file, then confirm so Trove starts the import."
url: https://docs.heytrove.ai/reference/api/upload
updated: 2026-10-01
---

# Upload API

> Load this page when: you need to add a file to the library over HTTP

## For agents

- Upload in three calls. Get an upload URL, PUT the file to it, then confirm.
- Send the same Content-Type to the upload URL that you declared in the first call.
- Poll trove items status or GET /items until the book is READY.

An upload has four steps. Step 1 is optional.

1. Call `POST /upload/check` to find likely duplicates.
2. Call `POST /upload` to reserve a book and get an upload URL.
3. `PUT` the file to that URL.
4. Call `POST /upload/confirm`. The import then runs in the background.

## Check for duplicates

`POST https://heytrove.ai/api/v1/upload/check`

Compares a file name with the titles in your library and returns likely duplicates. The check is advisory. It looks at your own books only.

**Parameters**

| Name | In | Type | Required | Constraints | Meaning |
| --- | --- | --- | --- | --- | --- |
| `filename` | body | string | yes | non-empty | File name, such as `use-the-index-luke.pdf`. |

**Request**

```bash
curl https://heytrove.ai/api/v1/upload/check \
  -H "Authorization: Bearer trove_your_key" \
  -H "Content-Type: application/json" \
  -d '{"filename":"use-the-index-luke.pdf"}'
```

**Response**

```json
{
  "candidates": [
    {
      "id": "cmabc123",
      "title": "Use The Index, Luke",
      "author": "Markus Winand",
      "status": "READY",
      "sourceType": "pdf",
      "createdAt": "2026-09-01T09:00:00.000Z",
      "score": 0.92
    }
  ]
}
```

**Errors**

| Status | Message | Fix |
| --- | --- | --- |
| 400 | `filename is required` | Send a file name. |
| 429 | `Too many requests` | Wait a minute. |

## Get an upload URL

`POST https://heytrove.ai/api/v1/upload`

Reserves a book in your library and returns a signed URL for the file. It uses one library slot.

**Parameters**

| Name | In | Type | Required | Constraints | Meaning |
| --- | --- | --- | --- | --- | --- |
| `filename` | body | string | yes | ends in `.pdf`, `.epub`, `.md` or `.markdown` | File name. It also sets the title. |
| `size` | body | integer | yes | more than 0, within your plan's upload cap | File size in bytes. |
| `contentType` | body | string | yes | `application/pdf`, `application/epub+zip`, `text/markdown` or `text/plain`, matching the extension | Type you will send. |

**Request**

```bash
curl https://heytrove.ai/api/v1/upload \
  -H "Authorization: Bearer trove_your_key" \
  -H "Content-Type: application/json" \
  -d '{"filename":"use-the-index-luke.pdf","size":2400000,"contentType":"application/pdf"}'
```

**Response**

```json
{
  "itemId": "cmabc123",
  "uploadUrl": "https://storage.example.com/signed-url",
  "storageKey": "uploads/user_id/cmabc123/use-the-index-luke.pdf",
  "expiresAt": "2026-10-01T10:00:00.000Z"
}
```

Then send the file. Use the same `Content-Type` you declared.

```bash
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @use-the-index-luke.pdf
```

**Errors**

| Status | Message | Code | Fix |
| --- | --- | --- | --- |
| 400 | `filename, size, and contentType are required` | none | Send all three. |
| 400 | `Only PDF, EPUB, and Markdown files are supported` | none | Convert the file. |
| 400 | `Invalid content type for {TYPE} file` | none | Match the content type to the extension. |
| 400 | `File exceeds {n}MB limit` | none | The cap depends on your plan. See [Plans and limits](https://docs.heytrove.ai/reference/plans-and-limits.md). |
| 400 | `filename must contain at least one visible character` | none | Use a real name. |
| 403 | `Your library is full. Upgrade to add more items.` | `PLAN_LIMIT_EXCEEDED` | See [Errors](https://docs.heytrove.ai/reference/api.md#errors). |
| 429 | `Too many library writes in a short window — wait a minute and retry.` | `RATE_LIMITED` | Wait for `Retry-After`. |

## Confirm an upload

`POST https://heytrove.ai/api/v1/upload/confirm`

Tells Trove the file is in place and starts the import. The book becomes READY when the import is done.

**Parameters**

| Name | In | Type | Required | Constraints | Meaning |
| --- | --- | --- | --- | --- | --- |
| `itemId` | body | string | yes | a book you own | The `itemId` from the upload call. |
| `storageKey` | body | string | yes | the `storageKey` from the upload call | Where the file is stored. |
| `replacesItemId` | body | string | no | a book you own | Remove this older copy after the new one is queued. |

**Request**

```bash
curl https://heytrove.ai/api/v1/upload/confirm \
  -H "Authorization: Bearer trove_your_key" \
  -H "Content-Type: application/json" \
  -d '{"itemId":"cmabc123","storageKey":"uploads/user_id/cmabc123/use-the-index-luke.pdf"}'
```

**Response**

```json
{
  "item": { "id": "cmabc123", "title": "Use The Index, Luke" },
  "job": { "id": "cmjob789", "status": "PENDING", "type": "INGEST" },
  "replacedItemId": null
}
```

If removing the older copy fails, the new book is still imported and `replacedItemId` is `null`.

**Errors**

| Status | Message | Code | Fix |
| --- | --- | --- | --- |
| 400 | `itemId and storageKey are required` | none | Send both. |
| 400 | `Invalid storageKey format` | none | Send the key exactly as you got it. |
| 404 | `Item not found` | none | Check `itemId`. |
| 409 | `This book is already being processed — wait for that to finish before starting another import.` | none | Wait, then check the status. |
| 403 | The locked-book message | `ITEM_LOCKED` | See [Errors](https://docs.heytrove.ai/reference/api.md#errors). |

**CLI equivalent**: [trove items add](https://docs.heytrove.ai/reference/cli/items-writing.md#trove-items-add).
