Upload Documents for Employees
BetaThe Documents API is in Beta. Endpoints, fields, and behavior may change in the upcoming weeks. See Beta Endpoints for what this means. Questions, feedback, or issues: [email protected].
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.
UsefeatureType: documentsA file uploaded for another feature (
users,chat, …) is rejected withFILE_NOT_FOUND. Generate the upload URL withfeatureTypeset todocuments.
Time LimitThe 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.eSignandgovernmentDocumentare 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
- Approve permission (account owners always have it) →
- Pushing onto a user whose current entry is
approvedorrejectedreplaces it with the new entry. - Pushing onto a user whose current entry is
pendingApprovalis refused withENTRY_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 knowThe response is the user's new row, in the same shape as Get user documents. Read
statusto know whether the entry was approved or is waiting for review.
Authentication
- API Key (
X-API-KEYheader), or - OAuth 2.0 with scopes
attachments.write(steps 2–4) anddocuments.write(step 5). Adddocuments.readto 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.
curl --request GET \
--url 'https://api.connecteam.com/documents/v1/documents?documentIds=52114' \
--header 'X-API-KEY: YOUR_API_KEY'{
"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
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"
}'{
"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.
curl --request PUT \
--url 'https://s3.amazonaws.com/bucket/path?X-Amz-Algorithm=...' \
--header 'Content-Type: application/pdf' \
--data-binary @./drivers-license-front.pdfStep 4 — Complete the upload
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'{
"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.
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 fileIds 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
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
{
"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
{
"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.
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"] }'{
"requestId": "6a8c0e2f-4b5d-4f7a-8c9e-1f3a5b7c9d1e",
"data": {
"userId": 3067712,
"documentId": 52115,
"documentType": "document",
"status": "approved",
"uploadedAt": 1792080000,
"expiresAt": 1798761600
}
}
Replacing an entry resets its expiryEach push records a new entry. For
entryexpiration, omittingexpiresAtleaves 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)
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.
| 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
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
expiresAtonly when the document'sexpiration.typeisentry.- Signing documents (
eSign,governmentDocument) cannot be pushed.- To track who still needs a document, see Track Document Compliance.
Push a document onto a user · Generate upload URL · Finalize upload
Updated 3 days ago
