Guides
Short polling
Keep another system up to date by polling for changed Fieldmotion records with durable, overlapping checkpoints.
When to use short polling
Short polling is a simple way to keep another system up to date with Fieldmotion data. Your integration stores a checkpoint timestamp, asks the API what changed at or after that checkpoint, fetches those records, processes them, and stores a new checkpoint for the next run.
This pattern suits server-to-server integrations that:
- need to pick up changes every few minutes;
- can tolerate a short delay after a CRM edit;
- need a simple process that can retry after network failures; and
- do not require a permanent connection, webhook, or custom Fieldmotion feature.
Avoid it when you need sub-second updates, intend to import a very large historical data set on every run, or require guaranteed deletion events. Most changed-list endpoints return active records only, so deletions may need a separate agreed process.
Authentication
Every call in this guide is a signed POST request. Follow Getting started to add _api_now, calculate _api_md5, add _api_id, and form-encode the request.
Convert integer and decimal values to strings before calculating the signature. The exact cpXX base host depends on the Fieldmotion account.
Basic polling flow
- Read the last successful checkpoint from durable storage.
- Subtract a small overlap, such as two minutes.
- Call an ID-only changed-list endpoint such as
Customers_listByLastEdited. - If IDs are returned, call the matching detail endpoint and request
last_edited. - Process every record idempotently.
- Store the maximum
last_editedvalue that was successfully processed.
Common changed-list endpoints
These endpoints accept a from value in yyyy-mm-dd hh:mm:ss format and return an array of IDs whose last_edited is greater than or equal to that value.
| Changed IDs endpoint | Follow-up detail endpoint | Notes |
|---|---|---|
Customers_listByLastEdited | Customers_getById | Non-deleted customers changed at or after from. |
Jobs_listByLastEdited | Jobs_getById | Non-deleted jobs changed at or after from; accepts optional form_id. |
Assets_listByLastEdited | Assets_getById | Non-deleted assets changed at or after from. |
AssetTypes_listByLastEdited | AssetTypes_getById | Asset type changes. |
Tasks_listByLastEdited | Tasks_getById | Non-deleted tasks changed at or after from. |
StockContainers_listByLastEdited | None in the current public API | Use only when the integration has an agreed stock-container follow-up path. |
Some list endpoints return records directly and accept a last_edited filter, including Customers_list, Jobs_list, Users_list, Vehicles_list, Departments_list, and Tasks_list.
For larger integrations, prefer an ID-only endpoint followed by a field-limited detail fetch. It keeps each polling step smaller and easier to retry.
Checkpoint rules
Store one checkpoint per object type:
customers_checkpoint = 2026-09-24 10:15:00
jobs_checkpoint = 2026-09-24 10:14:30
assets_checkpoint = 2026-09-24 10:15:10
- Use a deliberately old value such as
2000-01-01 00:00:00for an initial import. - Request
last_editedwhen fetching details. - Advance only after the records have been written successfully.
- Keep the old checkpoint when any request or processing step fails.
- Apply an overlap on every subsequent run.
- Prefer returned
last_editedvalues to your worker's local clock.
For most integrations, poll every two to ten minutes. Do not run a tight loop. Finish a returned batch before polling again, and use a local queue when thousands of records need steady downstream processing.
Deletions and archived records
Most changed-list endpoints filter out deleted records. Short polling is therefore useful for creates and updates but is not, by itself, a reliable deletion feed.
If the target system must mirror deletions exactly, agree a separate export, deletion-log, or custom endpoint strategy before implementation.
PHP example
This worker polls changed customers, fetches only the required details, and advances its checkpoint after successful processing.
<?php
$FMAPI = [
'api_id' => '123',
'api_key' => 'replace-with-your-api-key',
'base_url' => 'https://cp13.fieldmotion.com/a/',
];
function FM_api($functionName, $post = []) {
global $FMAPI;
$post['_api_now'] = ''.time();
$post['_api_md5'] = md5(json_encode($post).'|'.$FMAPI['api_key']);
$post['_api_id'] = $FMAPI['api_id'];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $FMAPI['base_url'].$functionName,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($post),
]);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
function loadCheckpoint($name) {
$file = __DIR__.'/'.$name.'.checkpoint';
return file_exists($file) ? trim(file_get_contents($file)) : '2000-01-01 00:00:00';
}
function saveCheckpoint($name, $value) {
file_put_contents(__DIR__.'/'.$name.'.checkpoint', $value);
}
function subtractOverlap($checkpoint, $seconds = 120) {
return date('Y-m-d H:i:s', strtotime($checkpoint) - $seconds);
}
$checkpoint = loadCheckpoint('customers');
$from = subtractOverlap($checkpoint);
$ids = FM_api('Customers_listByLastEdited', ['from' => $from]);
if (isset($ids['error'])) {
throw new RuntimeException($ids['error']);
}
if (!count($ids)) {
exit("No customer changes\n");
}
$customers = FM_api('Customers_getById', [
'ids' => json_encode(array_map('strval', $ids)),
'fields' => json_encode(['id', 'name', 'email', 'last_edited']),
]);
if (isset($customers['error'])) {
throw new RuntimeException($customers['error']);
}
$maxLastEdited = $checkpoint;
foreach ($customers as $customer) {
// Replace this with an idempotent upsert into your system.
printf("Customer %s changed at %s\n", $customer['id'], $customer['last_edited']);
if ($customer['last_edited'] > $maxLastEdited) {
$maxLastEdited = $customer['last_edited'];
}
}
saveCheckpoint('customers', $maxLastEdited);
Python example
This version uses only Python standard-library modules.
import hashlib
import json
import time
import urllib.parse
import urllib.request
from datetime import datetime, timedelta
from pathlib import Path
FMAPI = {
"api_id": "123",
"api_key": "replace-with-your-api-key",
"base_url": "https://cp13.fieldmotion.com/a/",
}
def fm_api(function_name, post=None):
post = dict(post or {})
post["_api_now"] = str(int(time.time()))
payload = json.dumps(post, separators=(",", ":"))
post["_api_md5"] = hashlib.md5(
(payload + "|" + FMAPI["api_key"]).encode("utf-8")
).hexdigest()
post["_api_id"] = FMAPI["api_id"]
data = urllib.parse.urlencode(post).encode("utf-8")
request = urllib.request.Request(
FMAPI["base_url"] + function_name, data=data, method="POST"
)
with urllib.request.urlopen(request, timeout=30) as response:
return json.loads(response.read().decode("utf-8"))
def load_checkpoint(name):
path = Path(f"{name}.checkpoint")
return path.read_text().strip() if path.exists() else "2000-01-01 00:00:00"
def save_checkpoint(name, value):
Path(f"{name}.checkpoint").write_text(value)
def subtract_overlap(checkpoint, seconds=120):
value = datetime.strptime(checkpoint, "%Y-%m-%d %H:%M:%S")
return (value - timedelta(seconds=seconds)).strftime("%Y-%m-%d %H:%M:%S")
checkpoint = load_checkpoint("customers")
ids = fm_api("Customers_listByLastEdited", {
"from": subtract_overlap(checkpoint),
})
if isinstance(ids, dict) and "error" in ids:
raise RuntimeError(ids["error"])
if ids:
customers = fm_api("Customers_getById", {
"ids": json.dumps([str(customer_id) for customer_id in ids], separators=(",", ":")),
"fields": json.dumps(["id", "name", "email", "last_edited"], separators=(",", ":")),
})
if isinstance(customers, dict) and "error" in customers:
raise RuntimeError(customers["error"])
max_last_edited = checkpoint
for customer in customers:
print(f"Customer {customer['id']} changed at {customer['last_edited']}")
if customer["last_edited"] > max_last_edited:
max_last_edited = customer["last_edited"]
save_checkpoint("customers", max_last_edited)
JavaScript / Node.js example
This version requires Node.js 18 or newer, where fetch is available globally.
const crypto = require('crypto');
const fs = require('fs');
const FMAPI = {
api_id: '123',
api_key: 'replace-with-your-api-key',
base_url: 'https://cp13.fieldmotion.com/a/',
};
async function fmApi(functionName, post = {}) {
post._api_now = String(Math.floor(Date.now() / 1000));
const payload = JSON.stringify(post);
post._api_md5 = crypto
.createHash('md5')
.update(payload + '|' + FMAPI.api_key)
.digest('hex');
post._api_id = FMAPI.api_id;
const response = await fetch(FMAPI.base_url + functionName, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(post),
});
return response.json();
}
function loadCheckpoint(name) {
const path = `${name}.checkpoint`;
return fs.existsSync(path) ? fs.readFileSync(path, 'utf8').trim() : '2000-01-01 00:00:00';
}
function saveCheckpoint(name, value) {
fs.writeFileSync(`${name}.checkpoint`, value);
}
function subtractOverlap(checkpoint, seconds = 120) {
const date = new Date(checkpoint.replace(' ', 'T') + 'Z');
date.setSeconds(date.getSeconds() - seconds);
return date.toISOString().slice(0, 19).replace('T', ' ');
}
(async () => {
const checkpoint = loadCheckpoint('customers');
const ids = await fmApi('Customers_listByLastEdited', {
from: subtractOverlap(checkpoint),
});
if (ids.error) throw new Error(ids.error);
if (!ids.length) return;
const customers = await fmApi('Customers_getById', {
ids: JSON.stringify(ids.map(String)),
fields: JSON.stringify(['id', 'name', 'email', 'last_edited']),
});
if (customers.error) throw new Error(customers.error);
let maxLastEdited = checkpoint;
for (const customer of customers) {
console.log(`Customer ${customer.id} changed at ${customer.last_edited}`);
if (customer.last_edited > maxLastEdited) maxLastEdited = customer.last_edited;
}
saveCheckpoint('customers', maxLastEdited);
})();
C# example
This version uses HttpClient and System.Text.Json.
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var apiId = "123";
var apiKey = "replace-with-your-api-key";
var baseUrl = "https://cp13.fieldmotion.com/a/";
async Task<JsonElement> FMApi(string functionName, Dictionary<string, string> post)
{
post["_api_now"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var payload = JsonSerializer.Serialize(post);
var bytes = MD5.HashData(Encoding.UTF8.GetBytes(payload + "|" + apiKey));
post["_api_md5"] = Convert.ToHexString(bytes).ToLowerInvariant();
post["_api_id"] = apiId;
using var client = new HttpClient();
using var content = new FormUrlEncodedContent(post);
using var response = await client.PostAsync(baseUrl + functionName, content);
response.EnsureSuccessStatusCode();
return JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement.Clone();
}
string LoadCheckpoint(string name)
{
var path = $"{name}.checkpoint";
return File.Exists(path) ? File.ReadAllText(path).Trim() : "2000-01-01 00:00:00";
}
void SaveCheckpoint(string name, string value) =>
File.WriteAllText($"{name}.checkpoint", value);
string SubtractOverlap(string checkpoint, int seconds = 120) =>
DateTime.ParseExact(checkpoint, "yyyy-MM-dd HH:mm:ss", null)
.AddSeconds(-seconds)
.ToString("yyyy-MM-dd HH:mm:ss");
var checkpoint = LoadCheckpoint("customers");
var idsResponse = await FMApi("Customers_listByLastEdited", new Dictionary<string, string>
{
["from"] = SubtractOverlap(checkpoint),
});
if (idsResponse.ValueKind == JsonValueKind.Object && idsResponse.TryGetProperty("error", out var error))
throw new Exception(error.GetString());
var ids = idsResponse.EnumerateArray().Select(id => id.ToString()).ToArray();
if (ids.Length > 0)
{
var customers = await FMApi("Customers_getById", new Dictionary<string, string>
{
["ids"] = JsonSerializer.Serialize(ids),
["fields"] = JsonSerializer.Serialize(new[] { "id", "name", "email", "last_edited" }),
});
if (customers.ValueKind == JsonValueKind.Object && customers.TryGetProperty("error", out var customerError))
throw new Exception(customerError.GetString());
var maxLastEdited = checkpoint;
foreach (var customer in customers.EnumerateArray())
{
var changed = customer.GetProperty("last_edited").GetString() ?? checkpoint;
Console.WriteLine($"Customer {customer.GetProperty("id")} changed at {changed}");
if (string.CompareOrdinal(changed, maxLastEdited) > 0) maxLastEdited = changed;
}
SaveCheckpoint("customers", maxLastEdited);
}
Error handling checklist
- Decode and log every API error response.
- Retry temporary network failures with backoff.
- Keep the previous checkpoint whenever a poll fails.
- Treat duplicate records as normal.
- Store enough information to replay a failed batch.
- Alert when polling has not succeeded for longer than expected.
Suggested production setup
A production integration normally has one scheduled worker and one durable checkpoint per object type, a local queue or staging table, idempotent target-system upserts, and monitoring for failures or stale checkpoints.
Every 5 minutes:
from = stored customers checkpoint - 2 minutes
ids = Customers_listByLastEdited(from)
customers = Customers_getById(ids, fields)
upsert customers into target system
store max successfully processed last_edited
This keeps the integration recoverable and avoids repeatedly transferring unchanged records.