---
updatedAt: 2026-10-15T14:20:01.000Z
agentTools:
  projectIndex: https://developer.connecteam.com/llms.txt
---

# Upload Documents for Employees

> 🚧 Beta
>
> **The Documents API is in Beta.** Endpoints, fields, and behavior may change in the upcoming weeks. See [Beta Endpoints](https://developer.connecteam.com/docs/beta-endpoints) for what this means. Questions, feedback, or issues: <omer.vered@connecteam.com>.

Upload a file you already hold, such as a scanned license or a background check PDF, onto an employee's document in Connecteam.

Pushing a document is a two-part flow: first upload the file through the general **Attachments** upload endpoints with `featureType: documents`, then attach the resulting `fileId` to the user's document.

## Endpoints

| Method | Endpoint                                                  | Description                                        |
| :----- | :-------------------------------------------------------- | :------------------------------------------------- |
| POST   | /attachments/v1/files/generate-upload-url                 | Get a `fileId` and a pre-signed upload URL         |
| PUT    | *(pre-signed URL)*                                        | Upload the file bytes directly to storage          |
| PUT    | /attachments/v1/files/complete-upload/{fileId}            | Finalize the upload                                |
| PUT    | /documents/v1/users/{userId}/documents/{documentId}/entry | Push the uploaded file(s) onto the user's document |

***

## Flow

```
1. Find the document       →  GET /documents/v1/documents (documentId, expiration.type)
2. Generate upload URL     →  POST /attachments/v1/files/generate-upload-url  (featureType: documents)
3. Upload the file         →  PUT file bytes to uploadFileUrl
4. Complete upload         →  PUT /attachments/v1/files/complete-upload/{fileId}
5. Push the entry          →  PUT /documents/v1/users/{userId}/documents/{documentId}/entry
6. (Optional) Verify       →  GET /documents/v1/user-documents?userIds={userId}
```

Repeat steps 2–4 for each file when an entry has several pages (up to 10 files), then push all `fileIds` in one call.

> 🚧 Use `featureType: documents`
>
> **A file uploaded for another feature (`users`, `chat`, …) is rejected** with `FILE_NOT_FOUND`. Generate the upload URL with `featureType` set to `documents`.

> ⚠️ Time Limit
>
> **The pre-signed URL expires after 300 seconds.** Upload and complete within that window, or generate a new URL.

***

## Overview

* Only `documentType: document` (plain upload) accepts a pushed file. `eSign` and `governmentDocument` are completed through the signing flow in Connecteam.
* The user must be assigned the document through a pack. Otherwise the call returns `DOCUMENT_NOT_ASSIGNED`.
* The resulting status depends on the caller's permission on the document's pack:
  * **Approve** permission (account owners always have it) → `approved`
  * **Upload** permission only → `pendingApproval`, for an admin to review in Connecteam
* Pushing onto a user whose current entry is `approved` or `rejected` **replaces** it with the new entry.
* Pushing onto a user whose current entry is `pendingApproval` is refused with `ENTRY_PENDING_REVIEW`, so an API call never overwrites something a reviewer is looking at. Approve or reject it in Connecteam first.
* The push triggers the same notifications, expiration reminders, and automations as an upload from the dashboard.

> 👍 Good to know
>
> **The response is the user's new row**, in the same shape as [Get user documents](documents-track-compliance). Read `status` to know whether the entry was approved or is waiting for review.

***

## Authentication

* **API Key** (`X-API-KEY` header), or
* **OAuth 2.0** with scopes `attachments.write` (steps 2–4) and `documents.write` (step 5). Add `documents.read` to look up documents and verify.

***

## Step 1 — Find the document

Get the `documentId`, and check `documentType` and `expiration.type`. They decide whether you can push and whether to send `expiresAt`.

```bash
curl --request GET \
  --url 'https://api.connecteam.com/documents/v1/documents?documentIds=52114' \
  --header 'X-API-KEY: YOUR_API_KEY'
```

```json
{
  "requestId": "1b3d5f7a-9c0e-4a2b-8d4f-6a8b0c2d4e6f",
  "data": {
    "documents": [
      {
        "documentId": 52114,
        "name": "Driver's License",
        "documentType": "document",
        "isReviewRequired": true,
        "expiration": { "type": "entry" },
        "createdAt": 1767225600,
        "modifiedAt": 1772409600
      }
    ]
  },
  "paging": { "offset": 1, "total": 1 }
}
```

***

## Step 2 — Generate an upload URL

```bash
curl --request POST \
  --url https://api.connecteam.com/attachments/v1/files/generate-upload-url \
  --header 'Content-Type: application/json' \
  --header 'X-API-KEY: YOUR_API_KEY' \
  --data '{
    "fileName": "drivers-license-front.pdf",
    "fileTypeHint": "application/pdf",
    "featureType": "documents"
  }'
```

```json
{
  "requestId": "2c4e6a8b-0d1f-4b3c-9e5a-7b9c1d3e5f7a",
  "data": {
    "fileId": "9f1c2b3a-4d5e-4f60-8a7b-1c2d3e4f5a6b",
    "uploadFileUrl": "https://s3.amazonaws.com/bucket/path?X-Amz-Algorithm=..."
  }
}
```

***

## Step 3 — Upload the file

Send the bytes straight to `uploadFileUrl`. This request goes to cloud storage, not the Connecteam API, so do not send your API key. `Content-Type` must match the file.

```bash
curl --request PUT \
  --url 'https://s3.amazonaws.com/bucket/path?X-Amz-Algorithm=...' \
  --header 'Content-Type: application/pdf' \
  --data-binary @./drivers-license-front.pdf
```

***

## Step 4 — Complete the upload

```bash
curl --request PUT \
  --url https://api.connecteam.com/attachments/v1/files/complete-upload/9f1c2b3a-4d5e-4f60-8a7b-1c2d3e4f5a6b \
  --header 'X-API-KEY: YOUR_API_KEY'
```

```json
{
  "requestId": "3d5f7a9c-1e2a-4c4d-8f6b-8c0d2e4f6a8b",
  "data": { "fileId": "9f1c2b3a-4d5e-4f60-8a7b-1c2d3e4f5a6b" }
}
```

A `fileId` that was never completed is rejected by the push with `FILE_NOT_FOUND`. Full Attachments details: [Upload file to the Cloud](upload-file-to-the-cloud).

***

## Step 5 — Push the entry onto the user

### Path Parameters

| Parameter    | Type    | Required | Description                              |
| :----------- | :------ | :------- | :--------------------------------------- |
| `userId`     | integer | Yes      | The user receiving the document          |
| `documentId` | integer | Yes      | The document to record the entry against |

### Request Body

| Field       | Type                  | Required | Description                                                                         |
| :---------- | :-------------------- | :------- | :---------------------------------------------------------------------------------- |
| `fileIds`   | array of UUID strings | Yes      | 1–10 completed `fileId`s with `featureType: documents`, in display order            |
| `expiresAt` | integer               | No       | Epoch seconds, in the future. Only for documents whose `expiration.type` is `entry` |

### Example — license with its own expiry date

```bash
curl --request PUT \
  --url https://api.connecteam.com/documents/v1/users/3067712/documents/52114/entry \
  --header 'Content-Type: application/json' \
  --header 'X-API-KEY: YOUR_API_KEY' \
  --data '{
    "fileIds": [
      "9f1c2b3a-4d5e-4f60-8a7b-1c2d3e4f5a6b",
      "0a2b4c6d-8e9f-4a1b-9c3d-5e7f9a1b3c5d"
    ],
    "expiresAt": 1830297600
  }'
```

### Response — caller has approve permission

```json
{
  "requestId": "4e6a8b0c-2f3b-4d5e-9a7c-9d1e3f5a7b9c",
  "data": {
    "userId": 3067712,
    "documentId": 52114,
    "documentType": "document",
    "status": "approved",
    "uploadedAt": 1792080000,
    "expiresAt": 1830297600
  }
}
```

### Response — caller has upload permission only

```json
{
  "requestId": "5f7a9c1d-3a4c-4e6f-8b8d-0e2f4a6b8c0d",
  "data": {
    "userId": 3067712,
    "documentId": 52114,
    "documentType": "document",
    "status": "pendingApproval",
    "uploadedAt": 1792080000,
    "expiresAt": 1830297600
  }
}
```

### Example — document with a shared expiration, or no expiration

Omit `expiresAt`. For `document` expiration, the response's `expiresAt` is the shared `nextExpiresAt`.

```bash
curl --request PUT \
  --url https://api.connecteam.com/documents/v1/users/3067712/documents/52115/entry \
  --header 'Content-Type: application/json' \
  --header 'X-API-KEY: YOUR_API_KEY' \
  --data '{ "fileIds": ["6a8c0e2f-4b5d-4f7a-9c9e-1f3a5b7c9d1e"] }'
```

```json
{
  "requestId": "6a8c0e2f-4b5d-4f7a-8c9e-1f3a5b7c9d1e",
  "data": {
    "userId": 3067712,
    "documentId": 52115,
    "documentType": "document",
    "status": "approved",
    "uploadedAt": 1792080000,
    "expiresAt": 1798761600
  }
}
```

> 📘 Replacing an entry resets its expiry
>
> **Each push records a new entry.** For `entry` expiration, omitting `expiresAt` leaves the new entry without a date; it does not inherit the previous entry's date. Always send the date printed on the new document.

***

## Step 6 — Verify (optional)

```bash
curl --request GET \
  --url 'https://api.connecteam.com/documents/v1/user-documents?userIds=3067712&documentIds=52114' \
  --header 'X-API-KEY: YOUR_API_KEY'
```

***

## Error Codes

Errors include `extraData.errorCode`. See [Error Format](documents-overview#error-format).

| HTTP Status | `errorCode`                    | Cause                                                                                                                                         | What to do                                          |
| :---------- | :----------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------- |
| `400`       | `FILE_NOT_FOUND`               | A `fileId` does not exist, was not completed, or was uploaded with a different `featureType`. `extraData.fileId` names the first failing file | Re-run steps 2–4 with `featureType: documents`      |
| `400`       | `DOCUMENT_NOT_ASSIGNED`        | The document is not in any pack assigned to the user                                                                                          | Assign the pack in Connecteam, or check the IDs     |
| `400`       | `DOCUMENT_TYPE_NOT_UPLOADABLE` | The document is `eSign` or `governmentDocument`                                                                                               | Complete it through the signing flow                |
| `400`       | `ENTRY_PENDING_REVIEW`         | The user's current entry is `pendingApproval`                                                                                                 | Approve or reject it in Connecteam, then push again |
| `400`       | `EXPIRATION_NOT_SUPPORTED`     | `expiresAt` sent for a document without `entry` expiration                                                                                    | Omit `expiresAt`                                    |
| `400`       | `EXPIRATION_DATE_IN_PAST`      | `expiresAt` is not in the future                                                                                                              | Send a future date, or skip expired documents       |
| `403`       | `INSUFFICIENT_PERMISSION`      | No upload or approve permission on the document's pack                                                                                        | Grant the caller permission on the pack             |
| `404`       | `DOCUMENT_NOT_FOUND`           | The document does not exist or was deleted                                                                                                    | Re-read the catalog                                 |
| `404`       | —                              | The user does not exist or is outside the caller's scope                                                                                      | Check the user ID                                   |
| `422`       | —                              | Empty `fileIds`, more than 10, or a value that is not a UUID                                                                                  | Fix the request body                                |

***

## Integration Example — Sync a license from your HR system

```javascript
import { readFile } from 'node:fs/promises';

const API = 'https://api.connecteam.com';
const headers = { 'X-API-KEY': 'YOUR_API_KEY', 'Content-Type': 'application/json' };

async function call(method, path, body) {
  const res = await fetch(`${API}${path}`, {
    method,
    headers,
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await res.json();
  if (!res.ok) {
    const err = new Error(json.error);
    err.code = json.extraData?.errorCode;
    throw err;
  }
  return json.data;
}

async function uploadForDocuments(path, mimeType) {
  const fileName = path.split('/').pop();
  const { fileId, uploadFileUrl } = await call('POST', '/attachments/v1/files/generate-upload-url', {
    fileName,
    fileTypeHint: mimeType,
    featureType: 'documents',
  });

  const put = await fetch(uploadFileUrl, {
    method: 'PUT',
    headers: { 'Content-Type': mimeType },
    body: await readFile(path),
  });
  if (!put.ok) throw new Error(`Storage upload failed: ${put.status}`);

  await call('PUT', `/attachments/v1/files/complete-upload/${fileId}`);
  return fileId;
}

async function pushLicense(userId, documentId, pages, expiresAt) {
  const fileIds = [];
  for (const page of pages) {
    fileIds.push(await uploadForDocuments(page, 'application/pdf'));
  }

  try {
    return await call('PUT', `/documents/v1/users/${userId}/documents/${documentId}/entry`, {
      fileIds,
      expiresAt,
    });
  } catch (err) {
    if (err.code === 'ENTRY_PENDING_REVIEW') {
      console.warn(`User ${userId} has an entry awaiting review; retry after it is reviewed`);
      return null;
    }
    throw err;
  }
}

const row = await pushLicense(
  3067712,
  52114,
  ['./licenses/3067712-front.pdf', './licenses/3067712-back.pdf'],
  Math.floor(new Date('2028-01-01T00:00:00Z').getTime() / 1000),
);
console.log(row?.status);
```

***

## Notes

> 📝 Important Considerations
>
> * Upload with `featureType: documents`. Files uploaded for other features are rejected.
> * Up to 10 files per entry; they are shown in the order you send them.
> * An entry waiting for approval is never overwritten by the API.
> * Send `expiresAt` only when the document's `expiration.type` is `entry`.
> * Signing documents (`eSign`, `governmentDocument`) cannot be pushed.
> * To track who still needs a document, see [Track Document Compliance](documents-track-compliance).

***

[Push a document onto a user](https://developer.connecteam.com/reference/put_user_document_entry_documents_v1_users__userid__documents__documentid__entry_put) · [Generate upload URL](https://developer.connecteam.com/reference/generate_upload_url_attachments_v1_files_generate_upload_url_post) · [Finalize upload](https://developer.connecteam.com/reference/finalize_upload_attachments_v1_files_complete_upload__fileid__put)