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

  1. Read the last successful checkpoint from durable storage.
  2. Subtract a small overlap, such as two minutes.
  3. Call an ID-only changed-list endpoint such as Customers_listByLastEdited.
  4. If IDs are returned, call the matching detail endpoint and request last_edited.
  5. Process every record idempotently.
  6. Store the maximum last_edited value 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 endpointFollow-up detail endpointNotes
Customers_listByLastEditedCustomers_getByIdNon-deleted customers changed at or after from.
Jobs_listByLastEditedJobs_getByIdNon-deleted jobs changed at or after from; accepts optional form_id.
Assets_listByLastEditedAssets_getByIdNon-deleted assets changed at or after from.
AssetTypes_listByLastEditedAssetTypes_getByIdAsset type changes.
Tasks_listByLastEditedTasks_getByIdNon-deleted tasks changed at or after from.
StockContainers_listByLastEditedNone in the current public APIUse 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:00 for an initial import.
  • Request last_edited when 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_edited values 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.