Skip to content

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.

Terminal window
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"
  • createdSince is an RFC 3339 date-time with an offset, e.g. 2026-05-01T00:00:00Z or 2026-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), then projectId.
  • Each project’s createdAt is when it was added to E1, in UTC to the second.

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.

  1. Read the timestamp you saved at the end of your last successful run. On the very first run, use a full sync instead.
  2. Subtract a safety overlap (an hour is plenty) and use that as createdSince.
  3. 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.
  4. 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
}
  • createdSince is 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.

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 original createdAt.
  • 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 projectId stops appearing, and its tenders move under the surviving project, which keeps its own, older createdAt.
  • 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.

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.