# 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 FieldMotion API what changed after that checkpoint, fetches the changed records, processes them, then stores a new checkpoint for the next run.

This guide is intended for server-to-server integrations. It does not require a permanent connection, webhooks, or a custom FieldMotion feature.

### When to use short polling

Use short polling when:

* You need another system to pick up changes from FieldMotion every few minutes.
* You can tolerate a short delay between a CRM edit and your system seeing it.
* You want a simple, reliable integration that can retry after network failures.

Avoid short polling when:

* You need sub-second updates.
* You need to import very large historical datasets on every run.
* You need guaranteed deletion events. Most changed-list endpoints return active records only, so deleted records may need a separate process or a custom integration.

### API authentication

FieldMotion API calls are signed POST requests. The current public API documentation front page shows the same pattern.

For every request:

1. Build the POST parameters.
2. Add `_api_now` as the current Unix timestamp. Send it as a string.
3. JSON-encode the POST parameters.
4. Append `|` and your API key to that JSON string.
5. MD5-hash the result.
6. Add `_api_md5` with the MD5 value.
7. Add `_api_id` with your API key ID.
8. POST the form data to `https://cpXX.fieldmotion.com/a/<FunctionName>`.

Important: convert integer and decimal values to strings before calculating `_api_md5`. This prevents JSON encoding differences between your client and the server.

Example base URL:

```
https://cp13.fieldmotion.com/a/
```

The exact `cpXX` server depends on the FieldMotion account.

### Basic polling flow

The safest loop is:

1. Read the last successful checkpoint from your database.
2. Subtract a small overlap, such as 2 minutes, from that checkpoint.
3. Call an ID-only changed-list endpoint, such as `Customers_listByLastEdited`.
4. If IDs are returned, call the matching `*_getById` endpoint and request `last_edited`.
5. Process each returned record idempotently.
6. Store the maximum `last_edited` value that was successfully processed.

The overlap is important. It protects you from clock differences, records edited in the same second, and a failure that happens after fetching IDs but before processing all records. Because overlap means you may see the same record more than once, your integration should deduplicate by object type, ID, and `last_edited`.

### Common changed-list endpoints

These endpoints accept:

```
from=yyyy-mm-dd hh:mm:ss
```

They return an array of changed IDs.

| Changed IDs endpoint               | Follow-up detail endpoint                            | Notes                                                                      |
| ---------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- |
| `Customers_listByLastEdited`       | `Customers_getById`                                  | Active customers changed after `from`.                                     |
| `Jobs_listByLastEdited`            | `Jobs_getById`                                       | Active jobs changed after `from`; accepts optional `form_id`.              |
| `Assets_listByLastEdited`          | `Assets_getById`                                     | Active assets changed after `from`.                                        |
| `AssetTypes_listByLastEdited`      | `AssetTypes_getById`                                 | Asset type changes.                                                        |
| `Tasks_listByLastEdited`           | `Tasks_getById`                                      | Active tasks changed after `from`.                                         |
| `StockContainers_listByLastEdited` | No generic detail endpoint in the current public API | Use only if your integration has an agreed stock-container follow-up path. |

Some list endpoints also accept a `last_edited` filter and return records directly, for example:

* `Customers_list`
* `Jobs_list`
* `Users_list`
* `Vehicles_list`
* `Departments_list`
* `Tasks_list`

For larger integrations, prefer the ID-only endpoint plus a detail fetch. That keeps each polling step smaller and easier to retry.

### Checkpoint rules

Store one checkpoint per object type. For example:

```
customers_checkpoint = 2026-05-03 10:15:00
jobs_checkpoint      = 2026-05-03 10:14:30
assets_checkpoint    = 2026-05-03 10:15:10
```

Recommended behavior:

* Start with a deliberately old checkpoint for the first import, such as `2000-01-01 00:00:00`.
* Always request `last_edited` when fetching details.
* Advance the checkpoint only after records are successfully written to your system.
* If a request fails, keep the old checkpoint and retry later.
* Use an overlap window on the next run.
* Do not use your local machine time as the final checkpoint if you can use the returned `last_edited` values instead.

### Polling interval

For most integrations, poll every 2 to 10 minutes. Do not poll continuously in a tight loop.

If a poll returns many records, finish processing the batch before immediately polling again. If your integration needs to process thousands of changes, add a queue on your side and process records at a steady rate.

### Deletions and archived records

Most changed-list endpoints filter out deleted records. That means short polling is good for detecting creates and updates, but not always sufficient for detecting deletions.

If your integration must mirror deletions exactly, discuss that requirement before implementation. A separate export, audit log, or custom endpoint may be needed depending on the object type.

### PHP example

This example polls changed customers, fetches their details, and advances a checkpoint after successful processing.

```php
<?php
$FMAPI = [
	'api_id' => '123',
	'api_key' => 'replace-with-your-api-key',
	'base_url' => 'https://cp13.fieldmotion.com/a/',
];

function FM_api($fn, $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'].$fn,
		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 your own upsert logic.
	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 example uses only Python standard library modules.

```python
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):
    dt = datetime.strptime(checkpoint, "%Y-%m-%d %H:%M:%S") - timedelta(seconds=seconds)
    return dt.strftime("%Y-%m-%d %H:%M:%S")


checkpoint = load_checkpoint("customers")
from_time = subtract_overlap(checkpoint)

ids = fm_api("Customers_listByLastEdited", {"from": from_time})
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 example uses Node.js 18 or newer, where `fetch` is available globally.

```javascript
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 body = new URLSearchParams(post);
	const response = await fetch(FMAPI.base_url + functionName, {
		method: 'POST',
		headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
		body,
	});

	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 dt = new Date(checkpoint.replace(' ', 'T') + 'Z');
	dt.setSeconds(dt.getSeconds() - seconds);
	return dt.toISOString().slice(0, 19).replace('T', ' ');
}

(async () => {
	const checkpoint = loadCheckpoint('customers');
	const from = subtractOverlap(checkpoint);

	const ids = await fmApi('Customers_listByLastEdited', { from });
	if (ids.error) {
		throw new Error(ids.error);
	}

	if (!ids.length) {
		console.log('No customer changes');
		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 example uses `HttpClient` and `System.Text.Json`.

```csharp
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();

    var json = await response.Content.ReadAsStringAsync();
    return JsonDocument.Parse(json).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)
{
    var dt = DateTime.ParseExact(checkpoint, "yyyy-MM-dd HH:mm:ss", null).AddSeconds(-seconds);
    return dt.ToString("yyyy-MM-dd HH:mm:ss");
}

var checkpoint = LoadCheckpoint("customers");
var from = SubtractOverlap(checkpoint);

var idsResponse = await FMApi("Customers_listByLastEdited", new Dictionary<string, string>
{
    ["from"] = from,
});

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 id = customer.GetProperty("id").GetString();
        var lastEdited = customer.GetProperty("last_edited").GetString() ?? checkpoint;

        Console.WriteLine($"Customer {id} changed at {lastEdited}");

        if (string.CompareOrdinal(lastEdited, maxLastEdited) > 0)
        {
            maxLastEdited = lastEdited;
        }
    }

    SaveCheckpoint("customers", maxLastEdited);
}
```

### Error handling checklist

Your polling worker should:

* Log every API error response.
* Retry temporary network failures with backoff.
* Keep the old checkpoint when a poll fails.
* Treat duplicate records as normal.
* Store enough information to replay a failed batch.
* Alert if polling has not succeeded for longer than expected.

### Suggested production setup

A typical production integration has:

* One scheduled worker per object type.
* One durable checkpoint per object type.
* A local queue or staging table for changed records.
* Idempotent upsert logic in the target system.
* Monitoring for API failures and stale checkpoints.

For example, a customer sync might run every 5 minutes:

```
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 pattern keeps the integration simple, recoverable, and gentle on the FieldMotion API.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.fieldmotion.com/fieldmotion-api-docs/short-polling.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
