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

# Uploading a document for an existing claim

> How to attach supporting documents, like proof of damage, to a claim through the API

Sometimes a claim needs evidence attached to it, a photo of the damage, an invoice, anything that helps the carrier review it. This page walks through how to upload one of those documents to an existing claim.

Uploading a document is a three-step process: you tell Opereit you're about to upload a file, you upload the file itself to a link you get back, and then you tell Opereit the upload is done.

For the full field-by-field breakdown, see the API reference for [Initiate document upload](/api-reference/endpoint/claims/documents/initiate) and [Confirm document upload](/api-reference/endpoint/claims/documents/complete).

## Before you start

You'll need:

* An **API key** (see [Authentication](/api-reference/introduction#authentication) if you don't have one yet).
* The **ID of an existing claim** to attach the document to (you get this back when you [create a claim](/guides/claims), it looks like `clm_3Ccox6eQUYYeg9RIo6qXCwGN3nQ`).
* The file itself, in one of the accepted formats: **PDF, JPEG, PNG, or WEBP**, and no bigger than **10 MB**.

## Step 1: Tell Opereit you're about to upload a file

```bash theme={null}
curl -X POST "https://api.opereit.ai/v1/claims/clm_3Ccox6eQUYYeg9RIo6qXCwGN3nQ/documents" \
  -u "your_key_id:your_key_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "content_type": "application/pdf",
    "file_name": "proof-of-damage.pdf"
  }'
```

This registers the document and hands you back a one-time upload link:

```json theme={null}
{
  "id": "doc_3Ccox6eQV1YYeg9RIo6qXCwGN4pQ",
  "claim_id": "clm_3Ccox6eQUYYeg9RIo6qXCwGN3nQ",
  "name": "proof-of-damage.pdf",
  "type": "pdf",
  "status": "PENDING_UPLOAD",
  "uploaded_by": "external",
  "created_at": "2026-09-14T10:00:00.000Z",
  "organization_id": "org_5f9c1a2b3c4d5e6f",
  "metadata": {
    "upload_url": "https://platform.opereit.ai/object/upload/sign/claim_documents/org_5f9c1a2b3c4d5e6f/clm_3Ccox6eQUYYeg9RIo6qXCwGN3nQ/doc_3Ccox6eQV1YYeg9RIo6qXCwGN4pQ.pdf?token=..."
  }
}
```

Two things worth knowing here:

* **Hang onto `id`.** You'll pass it back in step 3 to confirm the upload.
* **`content_type` has to be one of `application/pdf`, `image/jpeg`, `image/png`, or `image/webp`.** Anything else gets rejected with a `400` before anything is registered.

## Step 2: Upload the file

`metadata.upload_url` is a link that's ready to accept the raw file. Upload directly to it with an HTTP `PUT`:

```bash theme={null}
curl -X PUT "<the upload_url from step 1>" \
  -H "Content-Type: application/pdf" \
  --data-binary @proof-of-damage.pdf
```

Use the same `Content-Type` you sent in step 1. There's nothing to parse from the response here, a successful `PUT` just means the upload went through.

## Step 3: Confirm the upload

Once the file is uploaded, tell Opereit it's there:

```bash theme={null}
curl -X POST "https://api.opereit.ai/v1/claims/clm_3Ccox6eQUYYeg9RIo6qXCwGN3nQ/documents/complete" \
  -u "your_key_id:your_key_secret" \
  -H "Content-Type: application/json" \
  -d '{
    "document_ids": ["doc_3Ccox6eQV1YYeg9RIo6qXCwGN4pQ"]
  }'
```

You can confirm up to 10 documents in a single request, just list all their IDs. Opereit checks that each file was actually received, and reports back one result per document:

```json theme={null}
{
  "attached": 1,
  "documents": [
    { "id": "doc_3Ccox6eQV1YYeg9RIo6qXCwGN4pQ", "status": "confirmed" }
  ]
}
```

If a document didn't make it, it shows up with `"status": "failed"` and a `reason` instead, rather than silently being left out:

```json theme={null}
{
  "attached": 1,
  "documents": [
    { "id": "doc_3Ccox6eQV1YYeg9RIo6qXCwGN4pQ", "status": "confirmed" },
    { "id": "doc_9Xa22kdVWYYeg9RIo6qXCwGN8zT", "status": "failed", "reason": "not_found_in_storage" }
  ]
}
```

`attached` is just the count of documents with `"status": "confirmed"`, it's there so you don't have to count the array yourself if you don't need the per-document detail.

<Note>
  There's currently no endpoint to list the documents already attached to a claim. Keep track of the `id`s you get back from step 1 on your side if you need to reference them again later.
</Note>

## Document statuses

A document moves through exactly two states, tracked in the `status` field you get back in step 1:

| Status           | Meaning                                                                                                                  |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `PENDING_UPLOAD` | Registered, but the file hasn't been uploaded and confirmed yet. This is what you'll always see right after step 1.      |
| `CONFIRMED`      | The file was uploaded and Opereit has verified it's actually there. This happens once step 3 succeeds for that document. |

## Confirm results

The `status` inside each entry of `documents[]` in step 3's response is a different, smaller vocabulary, it's the outcome of that specific confirm attempt, not the document's lifecycle state:

| Status      | Meaning                                                                                                                 |
| ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| `confirmed` | The file was received, is a valid size, and matches an accepted content type. The document's status is now `CONFIRMED`. |
| `failed`    | Something went wrong, check the `reason` field for why.                                                                 |

### Failure reasons

`reason` is only present when `status` is `failed`.

| Reason                     | What it means                                                                                                   |
| -------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `document_not_found`       | No document with that ID exists on this claim. Double-check the ID and that it belongs to the claim in the URL. |
| `not_found_in_storage`     | The file was never actually uploaded to `upload_url`, or step 2 failed silently. Try step 2 again.              |
| `invalid_size`             | The uploaded file is missing a readable size, or is bigger than 10 MB.                                          |
| `unsupported_content_type` | The uploaded file's content type isn't one of the accepted types.                                               |
| `storage_check_failed`     | Opereit couldn't verify whether the file was received. Transient, safe to retry step 3.                         |
| `internal_error`           | Something went wrong on Opereit's end while confirming this document. Safe to retry step 3.                     |

## Handling errors

Both endpoints use the same error envelope as the rest of the API:

```json theme={null}
{
  "statusCode": 400,
  "statusReasonCode": "UNSUPPORTED_DOCUMENT_CONTENT_TYPE",
  "error": "Unsupported content type: text/plain. Expected pdf, jpeg, or png."
}
```

| statusReasonCode                                                                                                  | Status | What it means                                                                                                                                                    |
| ----------------------------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_REQUEST_BODY`                                                                                            | 400    | Something in the request body is missing or malformed, e.g. no `file_name`, or `document_ids` is empty or has more than 10 entries. Check `details.fieldErrors`. |
| `UNSUPPORTED_DOCUMENT_CONTENT_TYPE`                                                                               | 400    | `content_type` (step 1) isn't one of the accepted MIME types.                                                                                                    |
| `CLAIM_NOT_FOUND`                                                                                                 | 404    | No claim with that ID exists for your organization.                                                                                                              |
| `AUTHENTICATION_REQUIRED` / `INVALID_CREDENTIALS_FORMAT` / `INVALID_CREDENTIALS_ENCODING` / `INVALID_CREDENTIALS` | 401    | Something's wrong with your `Authorization` header.                                                                                                              |
| `INTERNAL_ERROR`                                                                                                  | 500    | Something broke on our end. Safe to retry.                                                                                                                       |

A rejected content type on a document you're actively confirming (step 3) doesn't come back as one of these top-level errors, it shows up as `"status": "failed", "reason": "unsupported_content_type"` for that specific document instead, see [Failure reasons](#failure-reasons) above.
