Integration health guide
Care4Fresh watches how each integration uses the API and, now and then, points out a habit that could be smoother, cheaper, or safer. These are suggestions, not errors, and nothing is blocked or throttled because of a suggestion itself. Our network security does separately block IP addresses that behave like scanners or brute-force attempts (for example, abnormally high request volumes from one address, or repeatedly ignoring rate limits), so the fastest way to stay clear of that is to follow the advice here.
When you get an advisory email or see a note in the portal, it will link to one of the sections below. Each one explains what the pattern is, why it is worth changing, and the concrete steps to fix it.
- The recommended sync flow
- Rate-limit responses are being ignored
- Polling without incremental sync
- Sync checkpoint is not moving forward
- Change window is never drained to the end
- Paging a change feed the slow way
- Re-fetching record detail that has not changed
- Requests to an address that does not exist
- The same request keeps failing
- Write requests are being rejected
The recommended sync flow
Most integrations keep a local copy of your Care4Fresh data (products, relations, sales, purchases, stock) and refresh it on a schedule. The reliable, low-cost way to do that is a one-time full sync followed by incremental deltas. The same shape works for every collection, so the examples below use /sales but apply to /products, /relations, /purchases, and /stock too.
1. Initial full sync (once)
On the very first run, read the entire collection and page through to the end. Omit modifiedSince so you get everything, and sort by last-modified ascending so paging stays stable while data changes underneath you.
GET /sales?sort=ModifiedAsc&page=1&pageSize=100, thenpage=2,page=3, and so on until a page comes back with fewer thanpageSizerows.- As you go, track the highest
modifiedAtyou have seen. When the full sync finishes, save that value as your checkpoint.
2. Incremental sync (every run after that)
On each later run, ask only for what changed since your checkpoint. This is a handful of requests instead of the whole data set.
GET /sales?modifiedSince=<checkpoint>&sort=ModifiedAsc&page=1&pageSize=100, paging to the end as above.modifiedSinceis an ISO 8601 timestamp (for example2026-05-04T12:00:00Z) and returns records whosemodifiedAtis strictly after it.- When the run finishes, advance your checkpoint to the new highest
modifiedAtfrom the response. If nothing changed, keep the previous checkpoint.
3. Pick up deletions
A record that is removed simply stops appearing in the list, so incremental reads alone never tell you it is gone. Each collection has a matching deletions feed for this.
GET /sales/deletions?modifiedSince=<checkpoint>returns the records deleted since that timestamp. Remove those from your local copy.modifiedSinceis required here; calling it without one returns400.- On your very first sync, skip the deletions feed. The initial full sync is your source of truth, so there is nothing yet to prune.
4. Reconcile occasionally
On a slower schedule (for example weekly), run a full sync again with modifiedSince omitted to catch any drift and confirm your copy still matches. Between reconciliations, the incremental poll plus the deletions feed keeps you current.
Two habits keep this healthy: honor the Retry-After header if you ever get a 429 (see below), and after a write such as creating a sales order, confirm the 2xx response and record the order so you do not resubmit it. The sections below explain what Care4Fresh flags when these slip.
Rate-limit responses are being ignored
What it means
When you send requests faster than your plan allows, the API answers with status 429 Too Many Requests and a Retry-After header telling you how many seconds to wait. This pattern shows up when an integration receives those 429s and keeps sending new requests straight away instead of pausing.
Why it matters
A 429 means the request was rejected, not served. If the workflow does not go back and retry it later, whatever it asked for is quietly missing from your copy of the data. Hammering through the limit also makes every response slower, so ignoring 429s tends to lose data and cost time at once.
How to fix it
- When a response is
429, read theRetry-Afterheader and wait that many seconds before trying the same request again. - If a request keeps getting rejected, back off further each time (for example wait 1s, then 2s, then 4s) rather than retrying immediately.
- Bring the request volume down so you rarely reach the limit. An incremental sync (see the next section) is the most effective way, because it fetches only what changed instead of everything every run.
Polling without incremental sync
What it means
An incremental sync asks the API for only the records that changed since your last run. This pattern shows up when an integration makes many list requests but never uses the modifiedSince parameter and never reads a deletions feed, so it re-pulls the full data set every time.
Why it matters
Re-pulling everything is far more requests than you need, which is what pushes an integration into the rate limit. It is also easy to miss deletions this way, because a record that was removed simply stops appearing and nothing tells you it is gone.
How to fix it
- Adopt the recommended sync flow: one full sync, then incremental deltas with a saved checkpoint.
- Pass
modifiedSincewith the timestamp of your last successful sync, together withsort=ModifiedAscso results come back oldest change first. - As you process the response, keep the highest
modifiedAtyou saw. Save it and use it as themodifiedSincevalue on your next run. - Poll the matching
/deletionsfeed for the same resource and remove those records locally, so deletions are handled instead of lingering.
Sync checkpoint is not moving forward
What it means
This pattern shows up when an integration does send modifiedSince on every request, but always sends the same value. The parameter is there; the checkpointing behind it is not. Every run then asks for everything that changed since that one fixed moment, rather than since the previous run.
Why it matters
The period being re-read grows by a day every day. Each run returns more data than the last, takes longer, and nearly all of it is data you already have. It gets worse on its own, and it is easy to miss because nothing fails: the responses are correct, just far larger and slower than they need to be.
How to fix it
- After each successful run, save the highest
modifiedAtyou saw (or the response'sasOf) and send that as the nextmodifiedSince. - Store the checkpoint somewhere that survives a restart or a redeploy. A checkpoint held only in memory, or written to a config file that gets overwritten on deploy, reverts to its starting value and produces exactly this pattern.
- Only move the checkpoint forward once the page has been processed successfully, so a failure part-way through does not skip records.
- If you want a safety margin against clock skew, subtract a few minutes from the saved value rather than falling back to a fixed date.
Change window is never drained to the end
What it means
This pattern shows up when an integration stops at the same page of a change feed on every run, while the point it syncs from never moves forward. It is the two problems above happening together, and the combination is worse than either alone.
Why it matters
A window that starts from a fixed point only grows, so the pages you do read are always the same oldest changes. Everything newer sits in the pages you never ask for. Results come back with a 200 and nothing looks wrong, which is why this is usually noticed weeks later as "our data stopped updating" rather than as an error. Because the feed is sorted oldest-first by default, the records lost are the most recent ones.
How to fix it
- Page until a response returns fewer rows than the
pageSizeyou asked for, rather than stopping after a fixed number of pages. That short page is the end of the window. - The response tells you where you are: compare
pagetimespageSizeagainsttotal. When a window is large, thehintfield spells out how many rows are still ahead of you. - Move your checkpoint forward after each successful run (see the section above). With a checkpoint that advances, each window is one poll wide and a page limit never comes near it.
- Cursor pagination removes the question: you drain until
nextCursorcomes backnull, so there is no page count to get wrong.
Paging a change feed the slow way
What it means
The price-changes feeds can be paged two ways: by page number, or with a cursor. This pattern shows up when an integration drains one of them with page= even though the cursor is available.
Why it matters
It is the slowest way to read the feed, and your own sync is what waits. Page numbers make the server re-run the whole comparison for every page, and they get slower the deeper the page number goes, so a run that drains several pages can spend seconds on each one. The cursor walks forward through a window prepared once instead. Page-number requests also cannot use the prepared-window cache that serves cursor readers, so every page pays full price on every run, however the administration is configured. Integrations that have switched typically see the same data arrive several times faster.
How to fix it
- Send
cursorwith an empty value on the first call (&cursor=), then keep calling with thenextCursorvalue from each response untilnextCursorcomes backnull. That is the whole window drained. - Drop the
pageparameter.pageSizestill applies, andmodifiedSinceis still required: it anchors the first page. - Cursor mode drains oldest change first, so leave
sortat its default. It cannot be combined with a descending sort.
Re-fetching record detail that has not changed
What it means
This pattern shows up when an integration reads the detail of the same records over and over, far more often than those records actually change. It is refreshing everything it tracks on every run rather than only the records that moved.
Why it matters
Most of those detail reads return data you already have, unchanged. They spend your request budget without adding anything, and they crowd out the requests that do carry new information.
How to fix it
- Use an incremental sync to learn which records changed, then fetch detail only for those. A record whose
modifiedAthas not advanced since your last sync does not need to be read again. - For records that are genuinely still in flux (for example an open order you are tracking to completion), refresh them on a sensible schedule rather than re-reading every record every run.
Requests to an address that does not exist
What it means
The URL your integration calls is not one the API serves, so every request comes back as not found. Nothing is read and nothing is saved.
Why it matters
This is not a request being refused, it is a request arriving nowhere, so there is nothing in the body to correct. Because each call fails quietly on its own rather than breaking anything visible, it can run for a long time before anyone notices that the data it was meant to move has never moved at all.
How to fix it
- Compare the URL you call against the documented one, character for character. The most common cause we see is a stray character on the end, picked up when the address was copied out of a sentence that ended in a full stop.
- Check the version segment appears exactly once, and that the base address is right for the environment you mean to use.
- If the finding says the address has also succeeded during the same period, the address is fine and the records being requested are gone. Treat those as deletions and stop asking for them.
- If the request is no longer needed at all, stop sending it.
The same request keeps failing
What it means
This pattern shows up when an integration sends a request that fails with the same client error (for example 400, 404, 409, or 422) on every run. It is usually a bad parameter or a reference to something that no longer exists, and often no one has noticed.
Why it matters
A request that always fails never returns data, so whatever it was meant to fetch or write is missing. Because it fails quietly in the background, it can go unnoticed for a long time while it wastes part of your request budget every run.
How to fix it
- Look at the sample requests in the advisory (or in the portal) to find the exact request and status.
- If it is a bad parameter or a stale reference, correct it. If the request is no longer needed, stop sending it.
- Add a check to your error handling so a request that fails the same way every run is surfaced to you rather than retried silently.
Write requests are being rejected
What it means
This pattern shows up when an integration sends create or update requests (for example creating a sales order) and the API rejects a large share of them with a client error such as 400, 409, or 422.
Why it matters
A rejected write is a change that was not saved. If the workflow does not correct the request and resend it, that data never lands in Care4Fresh, and it is easy to miss because each failure comes back one at a time in the background.
How to fix it
- The advisory lists the most common rejection reasons for the endpoint. Check each against the API reference. Most write rejections are a missing or malformed field, or a reference to something that does not exist.
- If the reason is that a record already exists (for example a duplicate order number), the write was already accepted once. Record each item as done when its create succeeds, and do not resend it, rather than retrying and being rejected.
- Confirm each write succeeded (a 2xx response) before moving on, so a rejected change is corrected and retried rather than silently dropped.
Want to stop receiving these suggestions? You can turn the advisory emails off from the API portal.