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

# Site files

> List, download, upload, and delete the documentation files attached to a site.

Site files are the documentation attached to a site — plans, photos, manuals, warranties.
They are the same files your team sees on the site record in the portal.

All six operations live under `/v1/site/{id}/file` and require the
[scopes](/permissions) `siteFile:read` (reads) or `siteFile:write` (uploads and deletes).
A key with `site:read` also has `siteFile:read`, and `site:write` also has
`siteFile:write`.

## Listing files

```bash theme={null}
curl -sS "https://connect.provisionapp.io/v1/site/$SITE_ID/file/list?limit=20&offset=0" \
  -H "x-api-key: $PROVISION_API_KEY"
```

Pinned files come first, then the newest. Deleted files are never returned.

```json theme={null}
{
  "data": [
    {
      "id": "9f1a2b3c-4d5e-4f60-8a1b-2c3d4e5f6071",
      "fileName": "site-plan.pdf",
      "contentType": "application/pdf",
      "size": 248310,
      "uploadedBy": "ovuid-8c1f...",
      "pinnedAt": null,
      "createdAt": "2026-02-17T12:30:45.123Z",
      "updatedAt": "2026-02-17T12:30:45.123Z"
    }
  ],
  "meta": { "total": 1, "limit": 20, "offset": 0 }
}
```

`uploadedBy` is `null` for files uploaded through this API — an API key is not a user, so
there is nobody to attribute the upload to.

## Downloading a file

`GET /v1/site/{id}/file/{fileId}` returns the same metadata plus a `downloadUrl`:

```bash theme={null}
curl -sS "https://connect.provisionapp.io/v1/site/$SITE_ID/file/$FILE_ID" \
  -H "x-api-key: $PROVISION_API_KEY"
```

```json theme={null}
{
  "id": "9f1a2b3c-4d5e-4f60-8a1b-2c3d4e5f6071",
  "fileName": "site-plan.pdf",
  "downloadUrl": "https://...",
  "downloadUrlExpiresAt": "2026-02-17T12:35:45.123Z"
}
```

<Warning>
  `downloadUrl` expires after **5 minutes**. Fetch the file again for a fresh URL rather
  than storing or sharing the one you got — a stored URL will start returning errors.
</Warning>

## Uploading a file

Uploads take three calls. The bytes go straight to storage rather than through this API,
which is what lets them exceed the API's 10 MB request limit. **There is no
single-call upload endpoint**, for files of any size.

<Steps>
  <Step title="Ask for an upload URL">
    ```bash theme={null}
    curl -sS -X POST "https://connect.provisionapp.io/v1/site/$SITE_ID/file/upload-url" \
      -H "x-api-key: $PROVISION_API_KEY" \
      -H 'content-type: application/json' \
      -d '{"contentType":"application/pdf"}'
    ```

    ```json theme={null}
    {
      "fileKey": "9f1a2b3c-4d5e-4f60-8a1b-2c3d4e5f6071",
      "uploadUrl": "https://...",
      "uploadUrlExpiresAt": "2026-02-17T12:35:45.123Z"
    }
    ```

    Nothing is stored yet. Keep the `fileKey` — step 3 needs it.
  </Step>

  <Step title="PUT the bytes to that URL">
    ```bash theme={null}
    curl -sS -X PUT "$UPLOAD_URL" \
      -H 'content-type: application/pdf' \
      --data-binary @site-plan.pdf
    ```

    The `Content-Type` header **must** match the `contentType` you sent in step 1, or the
    upload is rejected. Do not send your API key to this URL. `uploadUrl` expires after 5
    minutes; if it lapses, start again from step 1.
  </Step>

  <Step title="Register the file">
    ```bash theme={null}
    curl -sS -X POST "https://connect.provisionapp.io/v1/site/$SITE_ID/file" \
      -H "x-api-key: $PROVISION_API_KEY" \
      -H 'content-type: application/json' \
      -d '{"fileKey":"9f1a2b3c-4d5e-4f60-8a1b-2c3d4e5f6071","fileName":"site-plan.pdf"}'
    ```

    Returns `201` with the file, which now appears in the portal. Pass `"pinned": true` to
    pin it to the top of the site's file list.

    `size` and `contentType` are read back from storage, not taken from this request, so
    you cannot register a file that misreports either. If the upload in step 2 did not
    complete, this call returns `422 UPLOAD_NOT_FOUND` — retry step 2 with the same
    `uploadUrl` (or start over from step 1 if it has expired), then call this again.
  </Step>
</Steps>

An abandoned upload — step 2 done, step 3 never called — is simply never registered, and
the file will not appear anywhere. It is safe to retry the whole sequence.

## Extracted text

Text extracted from a file has its own endpoint, so that list and get responses stay small
even when a document runs to megabytes:

```bash theme={null}
curl -sS "https://connect.provisionapp.io/v1/site/$SITE_ID/file/$FILE_ID/content" \
  -H "x-api-key: $PROVISION_API_KEY"
```

```json theme={null}
{ "id": "9f1a2b3c-4d5e-4f60-8a1b-2c3d4e5f6071", "content": "SITE PLAN\nRev 3 ..." }
```

<Note>
  Expect `content` to be `null` most of the time. Text is extracted lazily — the first time
  someone auto-fills site documentation for that site — and only from plain-text and PDF
  files. Images and other formats always return `null`.
</Note>

## Deleting a file

```bash theme={null}
curl -sS -X DELETE "https://connect.provisionapp.io/v1/site/$SITE_ID/file/$FILE_ID" \
  -H "x-api-key: $PROVISION_API_KEY"
```

Returns `204` with no body.

<Warning>
  This is permanent, and matches what deleting a file in the portal does: both the file
  record and the stored bytes are removed. There is no undelete and no recycle bin.
</Warning>

## Errors

| Status | When |
| - | - |
| `400` | Malformed body or query — for example a `fileKey` that is not a uuid. |
| `403` | The key lacks `siteFile:read` or `siteFile:write`. |
| `404` | The site or the file does not exist, or belongs to another organization. |
| `422 UPLOAD_NOT_FOUND` | Registration was attempted before the upload completed. |
| `422 DUPLICATE_SITE_FILE` | That `fileKey` is already registered for the site. |

A site or file belonging to another organization returns `404`, not `403` — the API never
confirms that a record you cannot access exists.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.