Track Document Compliance

🚧

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

Use the two read endpoints together to answer compliance questions: who still owes a document, what one employee is missing, and what is about to expire.

Endpoints

MethodEndpointDescription
GET/documents/v1/documentsList the document templates you can view
GET/documents/v1/user-documentsList users' documents and their status

Overview

  • GET /documents is the catalog: names, types, review and expiration settings. It does not list users.
  • GET /user-documents is the per-user view: one row per user per assigned document, with that user's status.
  • Join the two on documentId to show a document's name next to each user's status.
  • Get user documents needs at least one of userIds or documentIds. Each accepts up to 100 IDs.
  • Every assigned document appears in the per-user view, including ones nobody has touched yet (pendingUserUpload).
  • Rows are sorted by userId, then documentId.
  • Fields with no value, such as uploadedAt before anything is submitted, are omitted rather than returned as null.
👍

Good to know

paging.total is always returned on both endpoints. Use it to get a count, for example "how many employees are missing their license", without paging through every row.

📘

Did you know?

pendingYourCompletion is relative to the caller. It means the document is waiting for the user behind the API key or OAuth token to sign. Another admin reading the same row sees pendingAdminFilling.


Authentication

  • API Key (X-API-KEY header), or
  • OAuth 2.0 with scope documents.read

Get documents

Query Parameters

ParameterTypeRequiredDescription
documentIdsarray of integersNoOnly include these document IDs
limitintegerNoResults per page, 1–200. Default 50
offsetintegerNoPagination offset. Default 0

Example Request

curl --request GET \
  --url 'https://api.connecteam.com/documents/v1/documents?limit=50' \
  --header 'X-API-KEY: YOUR_API_KEY'

Response

{
  "requestId": "7a1c9e3b-2d4f-4a6b-8c0d-1e2f3a4b5c6d",
  "data": {
    "documents": [
      {
        "documentId": 52114,
        "name": "Driver's License",
        "description": "Front and back of a valid license",
        "documentType": "document",
        "isReviewRequired": true,
        "expiration": { "type": "entry" },
        "createdAt": 1767225600,
        "modifiedAt": 1772409600
      },
      {
        "documentId": 52115,
        "name": "Food Handler Certificate",
        "documentType": "document",
        "isReviewRequired": false,
        "expiration": { "type": "document", "frequency": "yearly", "nextExpiresAt": 1798761600 },
        "createdAt": 1767225600,
        "modifiedAt": 1767225600
      },
      {
        "documentId": 52116,
        "name": "Employee Handbook Acknowledgement",
        "documentType": "eSign",
        "templateType": "sameForAll",
        "isReviewRequired": false,
        "createdAt": 1767312000,
        "modifiedAt": 1767312000
      },
      {
        "documentId": 52117,
        "name": "W-4",
        "documentType": "governmentDocument",
        "templateType": "w4",
        "isReviewRequired": false,
        "createdAt": 1767312000,
        "modifiedAt": 1767312000
      }
    ]
  },
  "paging": { "offset": 4, "total": 4 }
}

Response Fields

FieldTypeDescription
documentIdintegerUnique identifier of the document
namestringDocument name
descriptionstringDescription. Omitted when not set
documentTypestringdocument, eSign, or governmentDocument
templateTypestringsameForAll, perUser, w4, w9, or i9. Omitted for document
isReviewRequiredbooleanWhether an employee's upload needs admin approval
expiration.typestringdocument (shared recurring date) or entry (each entry has its own date). expiration is omitted when the document does not expire
expiration.frequencystringbiWeekly, monthly, or yearly. Only when type is document
expiration.nextExpiresAtintegerNext shared expiration, epoch seconds. Only when type is document
createdAt / modifiedAtintegerEpoch seconds

Get user documents

Query Parameters

ParameterTypeRequiredDescription
documentIdsarray of integersOne of documentIds / userIdsOnly include these documents, up to 100
userIdsarray of integersOne of documentIds / userIdsOnly include these users, up to 100
statusstringNoOnly include rows with this status. See Statuses
expiringBeforeintegerNoEpoch seconds. Only include rows whose expiration date is set and earlier than this. Covers both shared and per-entry expiration
limitintegerNoResults per page, 1–200. Default 50
offsetintegerNoPagination offset. Default 0

Pass arrays by repeating the parameter: ?documentIds=52114&documentIds=52115.

Response Fields

FieldTypeDescription
userIdintegerThe user the document is assigned to
documentIdintegerThe document
documentTypestringdocument, eSign, or governmentDocument
statusstringThe user's status on this document
uploadedAtintegerWhen the latest entry was submitted, epoch seconds. Omitted when nothing was submitted
signedAtintegerWhen the final signature was applied, epoch seconds. Only for eSign and governmentDocument whose signing is complete
expiresAtintegerWhen this user's document expires, epoch seconds. The shared date for document expiration, or the entry's own date for entry expiration. Omitted when there is no date

Recipe 1 — Who is still missing a document?

Filter by the document and by pendingUserUpload to list everyone who has not submitted it.

curl --request GET \
  --url 'https://api.connecteam.com/documents/v1/user-documents?documentIds=52114&status=pendingUserUpload' \
  --header 'X-API-KEY: YOUR_API_KEY'
{
  "requestId": "c2e4a6b8-0d1f-4a3b-8c5d-7e9f1a2b3c4d",
  "data": {
    "documents": [
      { "userId": 3067712, "documentId": 52114, "documentType": "document", "status": "pendingUserUpload" },
      { "userId": 3067715, "documentId": 52114, "documentType": "document", "status": "pendingUserUpload" }
    ]
  },
  "paging": { "offset": 2, "total": 2 }
}

Drop the status filter to see everyone assigned the document and where each person stands. Repeat the call with status=rejected to find people who need to re-submit.


Recipe 2 — One employee's documents

Filter by user to build an employee's compliance card.

curl --request GET \
  --url 'https://api.connecteam.com/documents/v1/user-documents?userIds=3067713' \
  --header 'X-API-KEY: YOUR_API_KEY'
{
  "requestId": "e4a6c8d0-2f3b-4c5d-9e7f-1a2b3c4d5e6f",
  "data": {
    "documents": [
      {
        "userId": 3067713,
        "documentId": 52114,
        "documentType": "document",
        "status": "approved",
        "uploadedAt": 1775001600,
        "expiresAt": 1793836800
      },
      {
        "userId": 3067713,
        "documentId": 52115,
        "documentType": "document",
        "status": "pendingApproval",
        "uploadedAt": 1791936000,
        "expiresAt": 1798761600
      },
      {
        "userId": 3067713,
        "documentId": 52116,
        "documentType": "eSign",
        "status": "approved",
        "uploadedAt": 1775088000,
        "signedAt": 1775091600
      },
      {
        "userId": 3067713,
        "documentId": 52117,
        "documentType": "governmentDocument",
        "status": "pendingAdminFilling",
        "uploadedAt": 1775174400
      }
    ]
  },
  "paging": { "offset": 4, "total": 4 }
}

Reading this: the license is approved and expires on its own date; the food handler certificate is waiting for approval and follows the shared yearly date; the handbook is signed; the W-4 is waiting for an admin signature.


Recipe 3 — What expires in the next 30 days?

Pass the cutoff as epoch seconds. Rows without an expiration date are excluded. Already-expired documents also match, because their date is earlier than the cutoff.

CUTOFF=$(( $(date +%s) + 30*24*3600 ))
curl --request GET \
  --url "https://api.connecteam.com/documents/v1/user-documents?documentIds=52114&documentIds=52115&expiringBefore=${CUTOFF}" \
  --header 'X-API-KEY: YOUR_API_KEY'
{
  "requestId": "a8c0e2f4-6b7d-4e9f-8a1b-3c4d5e6f7a8b",
  "data": {
    "documents": [
      {
        "userId": 3067713,
        "documentId": 52114,
        "documentType": "document",
        "status": "approved",
        "uploadedAt": 1775001600,
        "expiresAt": 1793836800
      }
    ]
  },
  "paging": { "offset": 1, "total": 1 }
}
📘

Shared expiration dates

For document expiration, every assigned user has the same expiresAt. Once the cycle rolls over, nextExpiresAt and each row's expiresAt move forward to the next date.


Pagination

Both endpoints use offset pagination. paging.offset in the response is where the next page starts; paging.total is the number of matching rows. Keep requesting with offset set to the returned paging.offset until it reaches paging.total.

curl --request GET \
  --url 'https://api.connecteam.com/documents/v1/user-documents?documentIds=52114&limit=200&offset=200' \
  --header 'X-API-KEY: YOUR_API_KEY'

Integration Example — Daily compliance report

Joins the catalog with the per-user view and lists missing and expiring documents per employee.

const BASE = 'https://api.connecteam.com/documents/v1';
const headers = { 'X-API-KEY': 'YOUR_API_KEY' };

async function getAll(path, params, key) {
  const rows = [];
  let offset = 0;
  while (true) {
    const qs = new URLSearchParams(params);
    qs.set('limit', '200');
    qs.set('offset', String(offset));
    const res = await fetch(`${BASE}/${path}?${qs}`, { headers });
    if (!res.ok) throw new Error(JSON.stringify(await res.json()));
    const body = await res.json();
    rows.push(...body.data[key]);
    offset = body.paging.offset;
    if (offset >= body.paging.total) return rows;
  }
}

const documents = await getAll('documents', [], 'documents');
const names = Object.fromEntries(documents.map((d) => [d.documentId, d.name]));

const trackedIds = documents
  .filter((d) => d.documentType === 'document')
  .map((d) => d.documentId);

const cutoff = Math.floor(Date.now() / 1000) + 30 * 24 * 3600;
const report = {};

for (let i = 0; i < trackedIds.length; i += 100) {
  const ids = trackedIds.slice(i, i + 100).map((id) => ['documentIds', String(id)]);
  const rows = await getAll('user-documents', ids, 'documents');
  for (const row of rows) {
    const missing = ['pendingUserUpload', 'rejected'].includes(row.status);
    const expiring = row.expiresAt !== undefined && row.expiresAt < cutoff;
    if (!missing && !expiring) continue;
    (report[row.userId] ??= []).push({
      document: names[row.documentId],
      status: row.status,
      expiresAt: row.expiresAt,
    });
  }
}

console.log(report);

Notes

📝

Important Considerations

  • Results only include packs the caller can view, and users within the caller's scope.
  • A document only appears for a user when it is in a pack assigned to that user.
  • Deleted documents are not returned.
  • signedAt is best-effort enrichment; if it cannot be resolved, the row is still returned without it.
  • To upload files onto these documents, see Upload Documents for Employees.

Get documents · Get user documents


Did this page help you?