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

# Document upload

> Upload utility bill PDFs and images for processing via the Nectar API.

In addition to automatic bill collection via [connections](/docs/developer-guide/getting-started#connection), you can upload documents directly through the API. This is useful for historical bills, one-off documents, or bills from utilities that don't have online portals.

## How document upload works

1. You submit one or more file URLs to the bulk upload endpoint.
2. Nectar queues each file for processing.
3. A **job ID** is returned that you can poll for status, or you can subscribe to `job.completed.v2`.
4. When processing completes, created documents appear in the API like any other bill. Jobs that produce no new documents (duplicates, non-utility files, password-protected files, or processing errors) still complete — they just leave `parsedDocumentIds` empty.

## Upload documents

Use the bulk upload endpoint to submit files for a company. Provide a `documents` array of publicly accessible HTTPS URLs. Optionally pin every document in the batch to a site and/or utility account, and pass free-text `notes` as background context for processing.

**Limits:** each document is at most **25MB**; PDF page limit is **800**.

Use **v2.2** for new integrations (v2.1 remains available).

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://external.nectarclimate.com/v2.2/job/company/{companyId}/bulk' \
    -H 'X-API-Key: YOUR_SECRET_KEY' \
    -H 'Content-Type: application/json' \
    -d '{
      "documents": [
        "https://your-storage.com/bills/jan-2025-electric.pdf",
        "https://your-storage.com/bills/feb-2025-electric.pdf"
      ],
      "siteId": "{siteId}",
      "accountId": "{accountId}",
      "notes": "Historical bills for Building A — Q1 2025"
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      f"https://external.nectarclimate.com/v2.2/job/company/{company_id}/bulk",
      headers={
          "X-API-Key": "YOUR_SECRET_KEY",
          "Content-Type": "application/json",
      },
      json={
          "documents": [
              "https://your-storage.com/bills/jan-2025-electric.pdf",
              "https://your-storage.com/bills/feb-2025-electric.pdf",
          ],
          "siteId": site_id,
          "accountId": account_id,
          "notes": "Historical bills for Building A — Q1 2025",
      },
  )
  jobs = response.json()
  ```
</CodeGroup>

### Request fields

| Field       | Type             | Required | Description                                                                     |
| ----------- | ---------------- | -------- | ------------------------------------------------------------------------------- |
| `documents` | Array of strings | Yes      | HTTPS URLs where Nectar can download each file                                  |
| `siteId`    | UUID             | No       | Pin every document in the job to this site instead of best-effort auto-matching |
| `accountId` | UUID             | No       | Pin every document in the job to this utility account                           |
| `notes`     | String           | No       | Free-text context shared with document processing                               |

## Upload a single file

You can also upload a single file directly as a form submission. The same **25MB** / **800-page** limits apply. Optional `siteId`, `accountId`, and `notes` work the same as bulk upload.

```bash theme={null}
curl -X POST 'https://external.nectarclimate.com/v2.2/job/company/{companyId}' \
  -H 'X-API-Key: YOUR_SECRET_KEY' \
  -F 'document=@/path/to/bill.pdf' \
  -F 'siteId={siteId}' \
  -F 'accountId={accountId}' \
  -F 'notes=Compliance upload for Building A'
```

## Check job status

After uploading, poll the job detail endpoint to check processing status. You can also subscribe to the `job.completed.v2` [webhook](/docs/developer-guide/webhooks) instead of polling.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X GET 'https://external.nectarclimate.com/v2.2/job/{jobId}' \
    -H 'X-API-Key: YOUR_SECRET_KEY'
  ```

  ```python Python theme={null}
  response = requests.get(
      f"https://external.nectarclimate.com/v2.2/job/{job_id}",
      headers={"X-API-Key": "YOUR_SECRET_KEY"},
  )
  job = response.json()
  print(job["status"])
  ```
</CodeGroup>

### Job status values

| Status      | Description                                                                             |
| ----------- | --------------------------------------------------------------------------------------- |
| `PENDING`   | Job is queued or in progress                                                            |
| `COMPLETED` | Processing finished. This includes jobs that created no new documents.                  |
| `FAILED`    | Processing failed — email [support@nectarclimate.com](mailto:support@nectarclimate.com) |

When a job completes, the response includes additional fields:

| Field                          | Type    | Description                                                               |
| ------------------------------ | ------- | ------------------------------------------------------------------------- |
| `parsedDocumentIds`            | Array   | IDs of documents created by this job. Empty when none were created.       |
| `duplicateDocumentIds`         | Array   | IDs of existing documents that uploaded files matched as duplicates       |
| `duplicates`                   | Integer | Count of duplicate files                                                  |
| `nonUtilityDocumentIds`        | Array   | IDs of files that are not utility bills                                   |
| `passwordProtectedDocumentIds` | Array   | IDs of files that could not be opened because they are password-protected |
| `errorDocumentIds`             | Array   | IDs of files that failed processing                                       |
| `terminationReason`            | String  | Reason the job failed. Present only when `status` is `FAILED`.            |

<Note>
  `COMPLETED` means Nectar finished processing the upload, not that a new document was created. A batch of duplicates or non-utility files still returns `COMPLETED` with the corresponding ID lists populated and `parsedDocumentIds` empty. `document.created.v2` fires only when a document is created; `job.completed.v2` fires for every finished job.
</Note>

## Supported file formats

| Format | Extension       | Notes                                             |
| ------ | --------------- | ------------------------------------------------- |
| PDF    | `.pdf`          | Most common format; supports multi-page documents |
| PNG    | `.png`          | Image of a utility bill                           |
| JPG    | `.jpg`, `.jpeg` | Image of a utility bill                           |

## Tips

* All URLs in the `documents` array must start with `https://`. If your files are in a private S3 bucket, generate a pre-signed URL.
* Include `siteId` and/or `accountId` when you need bills allocated to a known site or account — recommended for compliance-sensitive uploads.
* Use `notes` for background context (billing period, source, special instructions).
* Processing typically completes within a few minutes per document.
* Created documents appear in the standard `/document/` endpoints and trigger `document.created.v2` [webhooks](/docs/developer-guide/webhooks). Subscribe to `job.completed.v2` to learn when the upload job itself finishes, including when no document is created.

## Next steps

<CardGroup cols={3}>
  <Card title="Webhooks" icon="bell" href="/docs/developer-guide/webhooks">
    Get notified when upload jobs finish and when documents are created
  </Card>

  <Card title="Pagination" icon="arrow-right" href="/docs/developer-guide/pagination">
    Iterate through document lists
  </Card>

  <Card title="Data model" icon="diagram-project" href="/docs/developer-guide/data-model/overview">
    Understand how documents relate to usage data
  </Card>
</CardGroup>
