Upload Documents for Employees

🚧

Beta

The 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

MethodEndpointDescription
POST/attachments/v1/files/generate-upload-urlGet 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}/entryPush 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. 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.

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.pdf

Step 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

ParameterTypeRequiredDescription
userIdintegerYesThe user receiving the document
documentIdintegerYesThe document to record the entry against

Request Body

FieldTypeRequiredDescription
fileIdsarray of UUID stringsYes1–10 completed fileIds with featureType: documents, in display order
expiresAtintegerNoEpoch 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 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)

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 StatuserrorCodeCauseWhat to do
400FILE_NOT_FOUNDA fileId does not exist, was not completed, or was uploaded with a different featureType. extraData.fileId names the first failing fileRe-run steps 2–4 with featureType: documents
400DOCUMENT_NOT_ASSIGNEDThe document is not in any pack assigned to the userAssign the pack in Connecteam, or check the IDs
400DOCUMENT_TYPE_NOT_UPLOADABLEThe document is eSign or governmentDocumentComplete it through the signing flow
400ENTRY_PENDING_REVIEWThe user's current entry is pendingApprovalApprove or reject it in Connecteam, then push again
400EXPIRATION_NOT_SUPPORTEDexpiresAt sent for a document without entry expirationOmit expiresAt
400EXPIRATION_DATE_IN_PASTexpiresAt is not in the futureSend a future date, or skip expired documents
403INSUFFICIENT_PERMISSIONNo upload or approve permission on the document's packGrant the caller permission on the pack
404DOCUMENT_NOT_FOUNDThe document does not exist or was deletedRe-read the catalog
404—The user does not exist or is outside the caller's scopeCheck the user ID
422—Empty fileIds, more than 10, or a value that is not a UUIDFix 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 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.

Push a document onto a user · Generate upload URL · Finalize upload


Did this page help you?