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:
- Receive and process webhooks in real time — this keeps your system up to date moment-to-moment.
- Run a nightly reconciliation sweep — poll the list endpoints, compare with your local state, and upsert any differences.
- Verify entity links on demand — use
GET /integration-linksto check that your local foreign-key mappings are consistent with Align's integration_links table.
Endpoints to Poll
| Resource | Endpoint | Suggested cadence |
|---|---|---|
| Users (org members) | GET /api/v1/users | Nightly |
| Project members | GET /api/v1/projects/:id/members | Nightly per active project |
| Time entries | GET /api/v1/projects/:id/time-entries | Hourly — time data changes frequently |
| Wiki pages | GET /api/v1/projects/:id/wiki-pages | Nightly |
| Releases (evidence) | GET /api/v1/releases/:id/evidence | On demand after release.released event |
| Integration links | GET /api/v1/integration-links | Nightly, or after any provisioning call |
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
}
| Field | Type | Description |
|---|---|---|
data | array | Items for this page |
page | integer | Current page number (1-indexed) |
pageSize | integer | Items per page (max 100) |
total | integer | Total 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;
}
Verifying Entity Links with GET /integration-links
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.
Listing links
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:
- 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.
- 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.
- Mismatched
alignEntityId: Rare, but possible after a data migration. Update your local table.
Filtering options
| Query parameter | Description |
|---|---|
provider | Filter by the integration provider name (e.g. illumera) |
externalType | Filter by the entity type in your system (e.g. time_log, member) |
externalId | Look up a single link by your system's ID |
alignEntityType | Filter 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}`);
}
}
}