Track Document Compliance
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].
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
| Method | Endpoint | Description |
|---|---|---|
| GET | /documents/v1/documents | List the document templates you can view |
| GET | /documents/v1/user-documents | List users' documents and their status |
Overview
GET /documentsis the catalog: names, types, review and expiration settings. It does not list users.GET /user-documentsis the per-user view: one row per user per assigned document, with that user'sstatus.- Join the two on
documentIdto show a document's name next to each user's status. - Get user documents needs at least one of
userIdsordocumentIds. 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, thendocumentId. - Fields with no value, such as
uploadedAtbefore anything is submitted, are omitted rather than returned asnull.
Good to know
paging.totalis 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?
pendingYourCompletionis 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 seespendingAdminFilling.
Authentication
- API Key (
X-API-KEYheader), or - OAuth 2.0 with scope
documents.read
Get documents
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
documentIds | array of integers | No | Only include these document IDs |
limit | integer | No | Results per page, 1–200. Default 50 |
offset | integer | No | Pagination 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
| Field | Type | Description |
|---|---|---|
documentId | integer | Unique identifier of the document |
name | string | Document name |
description | string | Description. Omitted when not set |
documentType | string | document, eSign, or governmentDocument |
templateType | string | sameForAll, perUser, w4, w9, or i9. Omitted for document |
isReviewRequired | boolean | Whether an employee's upload needs admin approval |
expiration.type | string | document (shared recurring date) or entry (each entry has its own date). expiration is omitted when the document does not expire |
expiration.frequency | string | biWeekly, monthly, or yearly. Only when type is document |
expiration.nextExpiresAt | integer | Next shared expiration, epoch seconds. Only when type is document |
createdAt / modifiedAt | integer | Epoch seconds |
Get user documents
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
documentIds | array of integers | One of documentIds / userIds | Only include these documents, up to 100 |
userIds | array of integers | One of documentIds / userIds | Only include these users, up to 100 |
status | string | No | Only include rows with this status. See Statuses |
expiringBefore | integer | No | Epoch seconds. Only include rows whose expiration date is set and earlier than this. Covers both shared and per-entry expiration |
limit | integer | No | Results per page, 1–200. Default 50 |
offset | integer | No | Pagination offset. Default 0 |
Pass arrays by repeating the parameter: ?documentIds=52114&documentIds=52115.
Response Fields
| Field | Type | Description |
|---|---|---|
userId | integer | The user the document is assigned to |
documentId | integer | The document |
documentType | string | document, eSign, or governmentDocument |
status | string | The user's status on this document |
uploadedAt | integer | When the latest entry was submitted, epoch seconds. Omitted when nothing was submitted |
signedAt | integer | When the final signature was applied, epoch seconds. Only for eSign and governmentDocument whose signing is complete |
expiresAt | integer | When 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 datesFor
documentexpiration, every assigned user has the sameexpiresAt. Once the cycle rolls over,nextExpiresAtand each row'sexpiresAtmove 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.
signedAtis 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.
Updated 3 days ago
