Sync jobs
A write through the API lands in Fulify only. Sync jobs are how you send those changes to your sales channels without waiting for someone to press Sync in Fulify. You start a job with a list of SKUs, or ask for everything that's pending, and Fulify pushes the products in the background and reports the outcome for each one.
Sync jobs need the sync:write scope, which the Full integration preset includes.
How it fits together
A typical integration does two things:
- Writes changes with
POST /products. A product that was already synced, with Channel Sync on, becomespending. - Starts a sync job with
POST /sync-jobsto push those changes, then reads the result withGET /sync-jobs/{id}.
You decide when step 2 happens: straight after a write, on a schedule such as hourly or nightly, or after an export from your ERP. Fulify doesn't push on its own schedule.
A job pushes products exactly as pressing Sync on a product's channel tab does, with the same checks, the same data and the same sync status in the app.
Start a job
Push a list of SKUs:
Or push everything that's pending:
Send exactly one of skus or "all": true.
skustakes 1 to 1,000 SKUs. They're matched ignoring case and surrounding spaces, and a SKU sent twice is counted once."all": truetakes every pending product with Sync and Channel Sync on that is on at least one sales channel, oldest change first, up to 5,000 per job.
The response is 202 Accepted:
Products that can't be pushed are reported straight away in skipped, with a reason. Only the rest go into the job. For "all": true the response also has remaining: how many pending products were left out by the 5,000 cap. They're picked up by your next call.
If nothing is left to push, the job is created already completed, so a scheduled run always has a job id to log.
Which products are pushed
Fulify checks each product in this order. The first rule that matches decides.
| Reason | Meaning |
|---|---|
| not_found | No product with this SKU in your organization. |
| already_queued | The product is already waiting in another job. The entry includes that job's id. |
| inactive | The product's Sync switch (isActive) is off. |
| sync_off | Channel Sync (syncEnabled) is off. |
| no_channel | The product isn't on any BigCommerce or Shopify store. |
| never_synced | The product has never been synced to any of its stores. |
| not_pending | Nothing has changed since the last sync. |
Everything else is pushed, to every store the product is on.
- By SKU, a product is pushed when it's
pendingor its last sync failed (error). Listing a product by SKU is how you retry a failure. - With "all": true, only
pendingproducts are pushed. Products whose last sync failed are left out, so a scheduled job doesn't keep retrying the same problem. Retry them by SKU once fixed. - A product's first sync to a store happens in Fulify, with Add to Channel, which checks the store will accept it. A job only updates stores the product is already on. A store the product has been added to but never synced is listed under that product with the reason
never_synced, and the other stores are still pushed.
Read the result
results lists every product in the request, in the order you sent them, including the ones skipped at the start. This one response is the complete record of the job.
Job status:
| Status | Meaning |
|---|---|
| queued | Waiting to start. |
| running | Pushing products. |
| completed | Finished, with no failures. |
| completed_with_errors | Finished, and at least one store push failed. |
| failed | The job itself couldn't run. The response has an error explaining why. |
Product status:
| Status | Meaning |
|---|---|
| pending | Not pushed yet. |
| pushed | Every store succeeded. |
| partial | Some stores succeeded and some failed. |
| failed | Every store failed. |
| skipped | Not pushed. See reason. |
Each entry in channels has a status of pending, synced, synced_with_warnings (with warnings), error (with error) or skipped (with reason).
When a store's connection fails, for example because its credentials were revoked, the job stops trying that store. Its remaining products are marked error with the same message, instead of each failing in turn.
Results are kept for 30 days. After that, and for a job id from another organization, the response is 404 not_found.
How long a job takes
Products are pushed one store at a time, at the speed the store allows. Allow roughly 1 to 3 seconds per product per store, so 500 products on one store take about 10 to 25 minutes. Poll every 30 seconds or so, or use the sync_job.completed webhook instead of polling.
One job runs at a time per organization. A second job waits until the first has finished.
Dry runs
Add ?dryRun=true to see what a job would do without starting one:
A dry run applies every rule above and the scope check, but doesn't contact any store, so it can't predict errors a store would return. A store that would be skipped as never_synced is listed under notPushed.
Run a job once with Idempotency-Key
Send an Idempotency-Key header, up to 200 characters, to make a job safe to retry:
- The same key with the same request within 24 hours returns the original job with
200and starts nothing new. - The same key with a different request returns
409 idempotency_conflict.
A key per run works well, for example nightly-2026-10-23, so a rerun after a timeout or crash doesn't push twice. Without the header, each call starts a new job, but a product already waiting in a job is still skipped as already_queued.
Webhook
Subscribe an endpoint to the sync_job.completed event to hear when a job finishes, instead of polling. See Webhooks.
Limits
| Limit | Value |
|---|---|
| SKUs per request | 1,000 |
| Products per "all" job | 5,000 |
| POST /sync-jobs | 10 per minute per key |
| GET /sync-jobs/{id} | Counts in the read budget, 600 per minute |
| Jobs running at once | 1 per organization; others wait their turn |
| Results kept | 30 days |