Inventory sync that stops when the data looks wrong

It writes the fix. And it knows when not to.

Your warehouse says 40 in stock. Your shop says 12. Failclosed notices, writes 40 into the shop, and tells you it did. When the numbers look wrong for the wrong reason, it writes nothing and tells you which rule stopped it.

A normal day

Once a day, it reads both sides
warehouseSKU-1041 → 40
shopSKU-1041 → 12
Repaired. Set the shop to 40. Cost: €0.05.

The day it goes wrong

Your warehouse API has a bad minute and returns an empty list. Nothing is broken in your business — the response is just wrong.

The same run, one bad response
warehouse(no records)
shop5,000 products, all now “missing”
Refused. source_empty — nothing was written, you were told, and it cost nothing.

An ordinary sync tool cannot tell that empty list apart from a warehouse that sold everything, so it zeroes your shop. That has happened to real merchants across thousands of products at once. It is why most reconciliation tools stop at an email nobody reads — and it is the entire reason this one exists.

Everything it refuses to do

Eleven rules. Each has a code, so a refusal is something your systems can react to, not a note in a log. Five of them cannot be switched off — not by you, not for any price.

Refusals that stop the whole run

CodeWhat it protects you fromCan be switched off
source_emptyEvery record on the other side looks missing. Far more often a bad minute at an API than a real decision to delete everything you sell.always on
source_population_collapsedA partial response, a filter that silently applied, or a paginated read that stopped early. The records that vanished are not gone; they were simply not returned.optional
source_staleActing on a stale snapshot means correcting the present to match the past, which can be worse than leaving the disagreement alone.optional
duplicate_identifiersRecords cannot be matched reliably, so every conclusion drawn from the match is unsound.always on
too_few_recordsSmall samples make ordinary variation look like systemic disagreement.optional
divergence_too_wideWide disagreement usually means a source changed shape or failed, not that the world moved. Repairing on that basis multiplies the damage.optional
too_many_repairsA ceiling on how much can change in one pass, whatever the reason.optional
no_repairs_configuredNothing is repaired because nothing was configured to be repaired.always on

Refusals that stop one write

CodeWhat it protects you fromCan be switched off
destructive_not_permittedDeleting is the one repair that cannot be undone by repeating it differently. It requires saying so on the check, not merely on the action.always on
target_changed_since_readSomebody or something else has touched the record since we compared it. Writing now would overwrite a change we never saw.optional
non_idempotent_retryA timeout is ambiguous: the write may well have landed. Repeating a non-idempotent repair could apply it twice.always on
A service that will empty your catalogue if you ask it nicely enough is no safer than the cron script it replaces. Turning off one optional rule does not weaken the rest: the next guard still refuses, with its own code.

Setting it up

  1. Tell it which system to believe. Your ERP, your warehouse, whatever holds the real number. Any endpoint that returns JSON.
  2. Point it at Shopify. Four fields: your shop, an access token, a location, and where the quantity sits in your data.
  3. Watch it for a few days. It starts in watch-only mode and repairs nothing. You read what it would have done. When you trust it, send the same call with "repair": true.
# 1. The system you believe
curl -X POST https://YOUR-HOST/api/v1/sources \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "key": "warehouse",
        "url": "https://wms.example/api/stock",
        "secret_headers": { "Authorization": "Bearer ..." },
        "records_pointer": "/items", "key_pointer": "/sku" }'

# 2. Shopify, the repair and the schedule, in one call
curl -X POST https://YOUR-HOST/api/v1/connectors/shopify \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "key": "stock-sync",
        "shop": "acme-supplies",
        "access_token": "shpat_...",
        "location_id": "12345678",
        "authoritative_source": "warehouse",
        "quantity_pointer": "/quantity" }'

# => { "mode": "watch_only", ... }
Writing stock is safe to repeat: setting “available = 40” twice still leaves 40. That is what makes automatic write-back defensible here, and it is why this is the first thing the service supports.

Being told

Nothing here expects you to sit watching a screen. Give it one URL and it calls you when a refusal saved you, when a source is unhealthy, when a check paused itself, and when a repair failed.

curl -X POST https://YOUR-HOST/api/v1/notifications \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "url": "https://ops.example/hooks/failclosed" }'

Signed with HMAC-SHA256 so you can verify the call came from us. Deduplicated per subject, so a source that has been broken since Tuesday is one message, not one every time the check runs.

When you do want to read a week yourself, the report page renders it from the key you already hold: how often the two systems disagreed, on which records, and whether anything was written.

Price

Comparing one record1 millicent
A repair that reached your system€0.05
A refused repairFree
A run that could not read a sourceFree

A 5,000-product catalogue checked daily, repairing 300 lines a month, costs about €16.50: €1.50 of watching and €15.00 of repairs. Prepaid from €25.00. No subscription, no seat fee.

You pay for the write, because that is the work. Refusals are free on principle: charging for the safety feature would give you a reason to widen your limits, which is the opposite of the point.

What it cannot do

API

EndpointPurpose
GET /api/v1/rulesEvery refusal rule. No account.
POST /api/v1/tenantsCreate an account, receive an API key
POST /api/v1/connectors/shopifyShopify inventory in one call
POST /api/v1/sourcesRegister any JSON source
POST /api/v1/actionsRegister a repair
POST /api/v1/checksPair two sources, set the limits
GET /api/v1/divergencesOpen disagreements and their refusals
GET /api/v1/runsRun history, with what was refused and why
GET /api/v1/correctionsEvery repair sent, with what was in it
POST /api/v1/notificationsWhere to be told about refusals
GET /api/v1/balanceBalance, rates, ledger
POST /api/v1/credits/checkoutBuy credit