Incremental CRM sync
If you copy Speci-finder results into a CRM (Pipedrive, HubSpot, Salesforce or your own database) on a schedule, createdSince lets you pick up new projects quickly without reading every result. Pass it and the API returns only the projects added to E1 since then, oldest first.
This guide assumes you can already call the API. If not, start with Making your first call.
The request
Section titled “The request”curl -X GET \ "https://api.estimateone.com/api/tender/supplier/v1/companies/{companyId}/speci-finder/results?createdSince=2026-05-01T00:00:00Z&limit=100" \ -H "Authorization: Bearer $TOKEN" \ -H "x-e1-client-id: $CLIENT_ID"createdSinceis an RFC 3339 date-time with an offset, e.g.2026-05-01T00:00:00Zor2026-05-01T10:00:00+10:00. It’s inclusive: a project created at exactly that second is returned.- Results are ordered by
createdAt(oldest first), thenprojectId. - Each project’s
createdAtis when it was added to E1, in UTC to the second.
Paging through a sync window
Section titled “Paging through a sync window”A window can hold more projects than one page. Page through it as usual: repeat the request with cursor set to _metadata.nextCursor until hasNextPage is false. Keep createdSince (and limit) the same on every page; a cursor is tied to the createdSince it was issued for, and sending it with a different one returns 400 INVALID_CURSOR.
Each run
Section titled “Each run”- Read the timestamp you saved at the end of your last successful run. On the very first run, use a full sync instead.
- Subtract a safety overlap (an hour is plenty) and use that as
createdSince. - Page through every result, upserting each project into your CRM keyed on
projectId, so a project you’ve already seen is updated rather than duplicated. - When the last page is in, save the time the run started (not finished) as the timestamp for next time.
async function syncNewProjects(lastRunStartedAt) { const runStartedAt = new Date(); const createdSince = new Date(lastRunStartedAt.getTime() - 60 * 60 * 1000).toISOString();
let cursor; do { const params = new URLSearchParams({ createdSince, limit: '100' }); if (cursor) params.set('cursor', cursor);
const res = await fetch(`${BASE_URL}/api/tender/supplier/v1/companies/${COMPANY_ID}/speci-finder/results?${params}`, { headers: { Authorization: `Bearer ${token}`, 'x-e1-client-id': CLIENT_ID }, }); if (!res.ok) throw new Error(`Speci-finder sync failed: ${res.status}`);
const { data, _metadata } = await res.json(); for (const project of data) { await upsertIntoCrm(project.projectId, project); } cursor = _metadata.nextCursor; } while (cursor);
return runStartedAt; // save this; it's next run's lastRunStartedAt}Why overlap the windows?
Section titled “Why overlap the windows?”createdSinceis inclusive and timestamps are to the second, so two runs that meet exactly at a boundary could otherwise miss or double-count a project created in that second.- A new project reaches the search index shortly after it’s created, so it can first appear in results a little after its
createdAt. A run that starts at 09:00 might not yet see a project created at 08:59:58; the next run’s overlap picks it up.
Overlapping means you’ll see some projects twice. That’s expected, and it’s why step 3 upserts on projectId.
What an incremental run won’t catch
Section titled “What an incremental run won’t catch”createdSince filters on when the project was added to E1. Anything that changes an existing project leaves its createdAt alone, so an incremental run misses:
- New tenders on an existing project. Each builder runs their own tender, and builders often join a project weeks after it was first added. The new tender appears under the existing
projectId, with that project’s originalcreatedAt. - Projects that start matching later. A project created last month that only matches today, because a builder published a new document or you added a keyword, has an old
createdAt. - Merged projects. When E1 finds that two projects are the same job, it merges them. The duplicate’s
projectIdstops appearing, and its tenders move under the surviving project, which keeps its own, oldercreatedAt. - Changes and closures. Tender details change as builders update them, and tenders drop out of the results when they close.
So use the two together:
- Run a full sync (no
createdSince, page through everything) at least daily, and whenever you change your keywords. Treat it as the source of truth: it’s the only run that shows new tenders on existing projects. - Run incremental syncs between full syncs if you want brand-new projects to land sooner, e.g. hourly.
- When a project you hold is missing from a full sync, it has either closed or been merged into another project. Mark it as no longer current rather than deleting it, so any notes your team has added aren’t lost.
Results only ever include currently open tenders (see Data freshness); createdSince narrows that set, it doesn’t widen it.
Picking a schedule
Section titled “Picking a schedule”A daily full sync suits most CRM workflows. Add hourly incremental runs if your team acts on new projects the same day; an incremental run less often than your full sync adds nothing. See Data freshness and Rate limits.