Skip to main content

Reconciliation

Real-time webhooks are the fastest way to receive changes from Align. However, deliveries can occasionally be missed (network errors, your endpoint being briefly unavailable, or your system restarting). A nightly reconciliation pass ensures your local data converges with Align's state even after missed events.

Reconciliation Strategy

The recommended approach is:

  1. Receive and process webhooks in real time — this keeps your system up to date moment-to-moment.
  2. Run a nightly reconciliation sweep — poll the list endpoints, compare with your local state, and upsert any differences.
  3. Verify entity links on demand — use GET /integration-links to check that your local foreign-key mappings are consistent with Align's integration_links table.

Endpoints to Poll

ResourceEndpointSuggested cadence
Users (org members)GET /api/v1/usersNightly
Project membersGET /api/v1/projects/:id/membersNightly per active project
Time entriesGET /api/v1/projects/:id/time-entriesHourly — time data changes frequently
Wiki pagesGET /api/v1/projects/:id/wiki-pagesNightly
Releases (evidence)GET /api/v1/releases/:id/evidenceOn demand after release.released event
Integration linksGET /api/v1/integration-linksNightly, or after any provisioning call
tip

For time entries, use the startDate and endDate filters to limit each poll to a rolling window (e.g. the last 7 days) rather than fetching the full history on every run.

Pagination Model

All list endpoints use offset-based pagination with page and pageSize query parameters.

GET /api/v1/projects/:id/time-entries?page=1&pageSize=100

Response shape:

{
"data": [ /* items */ ],
"page": 1,
"pageSize": 100,
"total": 347
}
FieldTypeDescription
dataarrayItems for this page
pageintegerCurrent page number (1-indexed)
pageSizeintegerItems per page (max 100)
totalintegerTotal number of matching records across all pages

To fetch all pages:

async function fetchAll(url, apiKey) {
const results = [];
let page = 1;
const pageSize = 100;

while (true) {
const res = await fetch(`${url}?page=${page}&pageSize=${pageSize}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
results.push(...body.data);
if (results.length >= body.total) break;
page++;
}
return results;
}

When you provision Align entities (users, projects, time entries) via the API using an externalRef, Align stores an integration_links row mapping your external ID to the Align entity ID. The GET /integration-links endpoint lets you fetch and verify these mappings.

GET /api/v1/integration-links?provider=illumera&externalType=time_log&page=1&pageSize=100
Authorization: Bearer <api-key>

Response:

{
"data": [
{
"id": "link-uuid-...",
"provider": "illumera",
"externalType": "time_log",
"externalId": "illumera-log-9876",
"alignEntityType": "time_entry",
"alignEntityId": "te-uuid-...",
"projectId": "proj-uuid-...",
"organizationId": "org-uuid-...",
"createdAt": "2026-05-26T09:00:00.000Z"
}
],
"page": 1,
"pageSize": 100,
"total": 1
}

Reconciliation check

During your nightly sweep, compare the integration-links response with your local foreign-key table:

  1. Missing from Align (your side has a link that Align doesn't): The Align entity may have been deleted. Remove or flag the link in your system.
  2. Missing from your side (Align has a link that you don't): Your system missed the creation event. Fetch the Align entity and upsert it locally.
  3. Mismatched alignEntityId: Rare, but possible after a data migration. Update your local table.

Filtering options

Query parameterDescription
providerFilter by the integration provider name (e.g. illumera)
externalTypeFilter by the entity type in your system (e.g. time_log, member)
externalIdLook up a single link by your system's ID
alignEntityTypeFilter by Align entity type (e.g. time_entry, user)

Sample Reconciliation Script (Node.js)

import fetch from "node-fetch";

const BASE = "https://app.alignsoft.us/api/v1";
const KEY = process.env.ALIGN_KEY;

async function reconcileTimeEntries(projectId, localDb) {
const sevenDaysAgo = new Date(Date.now() - 7 * 86400_000)
.toISOString()
.slice(0, 10);

const alignEntries = await fetchAll(
`${BASE}/projects/${projectId}/time-entries?startDate=${sevenDaysAgo}`,
KEY,
);

for (const entry of alignEntries) {
const local = await localDb.getTimeEntry(entry.id);
if (!local || local.status !== entry.status || local.hours !== entry.hours) {
await localDb.upsertTimeEntry(entry);
console.log(`Reconciled time entry ${entry.id}`);
}
}
}

async function reconcileIntegrationLinks(provider, externalType, localDb) {
const alignLinks = await fetchAll(
`${BASE}/integration-links?provider=${provider}&externalType=${externalType}`,
KEY,
);

const alignMap = new Map(alignLinks.map((l) => [l.externalId, l]));
const localLinks = await localDb.getLinks(provider, externalType);
const localMap = new Map(localLinks.map((l) => [l.externalId, l]));

// Present in your DB but missing from Align — entity was deleted
for (const [externalId, local] of localMap) {
if (!alignMap.has(externalId)) {
await localDb.flagLinkMissing(local.id);
console.warn(`Link missing from Align: ${externalId}`);
}
}

// Present in Align but missing from your DB — missed creation event
for (const [externalId, align] of alignMap) {
if (!localMap.has(externalId)) {
await localDb.insertLink(align);
console.log(`Backfilled link: ${externalId}${align.alignEntityId}`);
}
}
}