Synchronisation with an online shop
Everything is configured in Sklad, Довідники → Інтернет-магазини. The configuration lives in the database, alongside the other configuration blocks.
How it works
flowchart TD
SK["Sklad<br/>(administration)"]
subgraph DBG["DB"]
PL["Platforms<br/>R_INET_PLATFORMS"] --> SF["Online shops<br/>R_INET_STOREFRONTS"]
SF --> CFG["Connector<br/>S_BLOCKYAML"]
D[("Goods, orders")]
end
subgraph ABG["abasyn"]
ABS["Sync jobs<br/>(one per shop), notifications<br/> (email, telegram)"]
end
ABR["abrest<br/>finds or creates goods, normalises and analyses data"]
M["Online shop"]
D ~~~ ABR
ABR ~~~ M
SK ==>|edits the platform,<br/>the shop, the connector| PL
SK ==>|config template, job status, manual run, de/activation of the sync| ABS
CFG -.->|one block =<br/>one job| ABS
ABS --> ABR
ABR -.-> ABS
ABR --> D
D -.-> ABR
M -->|goods, categories,<br/>characteristics, images,<br/>orders| ABS
ABS -.->|price, stock, names,<br/>keywords, listing,<br/>images| M
Solid arrows are the data flow from the shop into the database; dashed arrows are the DB → shop flow; thick arrows are administrator actions. Sklad edits the platform, the shop and the connector directly in the DB, and controls abasyn's jobs over the HTTP REST API. The sync's internal business logic — finding or creating a good, matching, the status vocabulary — runs in abrest; abasyn stays transport between the shop and abrest.
One platform can have several shops; each shop has only one connector settings block, and that block becomes the sync job in abasyn (the job key is the online shop's id).
The expected working cycle: 1. Preparation and testing - through the probe, readonly and rehearse modes in turn. 2. Commissioning — one full catalog pull creates the goods, the category tree and the mappings to the shop. 3. Steady state, inbound — orders, their lines and shipments arrive continuously; the catalog pull keeps picking up goods that appeared in the shop by other means. 4. Steady state, outbound — a price, name, description, keyword or photo change, or a new listing, made in Sklad is sent on the next run.
Who owns what
At mode: live everything is synchronised by default. Switch off what you do not need
| Data | Master | Direction | Switched off by |
|---|---|---|---|
| Good name, short description, description, keywords | Abacus | out | skip_push: [names] |
| Price | last writer wins | both | skip_push: [products] |
Stock (SUM(M_TOVAR_AGG.TOVAR_AMOUNT)) |
Abacus | out | skip_push: [stock] |
| Listing / delisting | Abacus (IS_LISTED) |
out | skip_push: [listing] |
| Order status | two-master, newest stamp wins | both | skip_push: [order_status] |
| Images | Abacus | out (WooCommerce only) | skip_push: [images] |
| Product creation | shop, plus Abacus on WooCommerce | both | skip_push: [listing] |
| Orders, lines, shipments | shop | in | skip_pull: [orders] |
| Product photos | shop | in | skip_pull: [images] |
| Product characteristics | shop | in, additive | skip_pull: [characteristics] |
| Unpublished products | shop | in | skip_pull: [drafts] |
| Category tree | shop | in | not switchable |
Prerequisites
- abrest and abasyn running on the server
- Your work profile has an
abasyn:section
- An abasyn access token for the operator's user. Tokens are created by the activate services button in the configuration editor.
- Shop credentials:
- Prom.ua — one API token from the seller cabinet.
- WooCommerce — a WordPress login and an application password
for that user, generated under Users → Profile → Application Passwords.
Give the user the Shop Manager role: it carries
manage_woocommerce,edit_productsandupload_files- this is what the sync needs.
1. Platform
The top-left grid is platforms (Prom, Rozetka, Allegro, eBay), which can hold many separate shops.
| Column | Meaning |
|---|---|
| Майданчик | Free-text name. |
| Адреса сайту | The platform's own site. Informational only. |
| Тип конектора | prom, woocommerce or stub. Empty means no shop on this platform is synced. |
stub is a built-in fake source for exercising the pipeline without creating a
real online shop.
2. Online shop
The top-right grid is the shops on the selected platform. Separate records are separate shops; their goods, prices and orders never interleave.
| Column | Meaning |
|---|---|
| Назва інтернет-магазину | Free-text name. Information only, rename it at any time. |
| Головний URL | R_INET_STOREFRONTS.URL_MAIN. Load-bearing: on WooCommerce it is the API host (abasyn appends /wp-json/wc/v3 — do not add it); on Prom it is the shop front used for product links and page reads. |
| URL адмінки | Admin/API address, if it differs from the main URL. |
| Активний | IS_ACTIVE. The main on/off switch for this shop's sync. |
3. Credentials
Select the shop, press Токен доступу до API. Prom asks for a single API
token. A woocommerce platform asks for two values: the WordPress login,
then that user's application password. Both are stored as shared rows
(S_TOKENS, user_id = -1, service_name = 'storefront' /
'storefront_secret') and are available to every work profile.
The WooCommerce password must be an application password, generated under Users → Profile → Application Passwords in WordPress. The account login password is not accepted — WordPress validates only application passwords on this path, and anything else fails with
invalid_username. WordPress prints the value in groups of four characters; the spaces are cosmetic and may be kept or stripped.One application password covers the whole integration: the WooCommerce API for catalog, orders, prices and listings, and the WordPress media library for product images. Revoking it in WordPress cuts the integration off completely without disturbing anything else.
4. Connector settings
Select the shop, press Налаштування синхронізації. With no block yet, Sklad
fetches a starter one from GET /api/connectors/template/<conntype> — so
creating the first block requires abasyn to be reachable. Ctrl+J on a value
lists the allowed options.
A missing settings block means "not configured", not "defaults": an online shop with no settings stays unsynchronised even if the user activates it. Creating the block starts the sync.
interval: 600
mode: probe
skip_push: []
skip_pull: []
lang: uk
remote_url: ''
landing_group_id: 0
tag_group_id: 0
push_miss_limit: 3
scrape_delay_ms: 500
order_backfill_days: 30
order_status_default: ''
retry_interval_minutes: 60
max_retries: 24
max_age_hours: 24
Everything is on by default. An active online shop with a filled-in block and
mode: live synchronises both directions in full (from the site into Abacus and
from Abacus to the site).
| Key | Effect |
|---|---|
interval |
Seconds between runs, counted from the end of the previous one. 0 = on demand only. |
mode |
Commissioning stage. See below. |
skip_push |
What not to write to the shop: order_status, products (price), stock, names, listing, images. Empty = the shop gets all of it. |
skip_pull |
What not to read from the shop: images, characteristics (Prom only), drafts, orders. Empty = everything the shop offers is read. |
lang |
Preferred language for multilingual sources (Prom name/description). Keywords do not follow it — Prom ships no keywords_multilang. |
remote_url |
Overrides the API base — https://my.prom.ua/api/v1 for Prom, shop URL + /wp-json/wc/v3 for Woo. Empty derives it automatically; change it only if you know exactly what you are doing. |
landing_group_id |
Id of the group that newly created goods are filed under. |
tag_group_id |
Id of the group that scopes the characteristics of goods with no group. 0 means "take landing_group_id"; only when that is 0 too do the characteristics apply in every group. Set it explicitly when the tags belong somewhere other than the landing group. |
push_miss_limit |
How many times in a row to keep trying to match a shop product to a good in the database when the shop answers "no such product". Counted in CR_TOVAR_STOREFRONTS.PUSH_MISS_COUNT, reset by any successful write. Only an unknown-id answer counts — a validation rejection never retires a live mapping. |
scrape_delay_ms |
Prom only. Milliseconds between product-page reads during a manual characteristics pull. |
order_backfill_days |
How many days of orders to fetch from the shop on the first sync. 0 = whole history. |
order_status_default |
Default order status when the shop does not supply one. |
retry_interval_minutes, max_retries, max_age_hours |
Retry cadence for orders that failed to save. |
An invalid value inside skip_push / skip_pull is logged and ignored,
switching nothing off.
Saving the configuration from Sklad posts api/connectors/reload, and abasyn
re-reads it without a restart. Only the configuration itself is updated —
LastRun, watermarks and stats survive, so a save does not re-pull the catalog.
If abasyn is unreachable, the settings are still saved and the change takes
effect at the next service start.
Sklad refuses to save invalid YAML.
mode — the commissioning stages
| mode | volume | writes to the shop |
|---|---|---|
probe |
10 goods, 10 orders | never |
readonly |
full | never |
rehearse |
full | evaluates and logs, sends nothing |
live |
full | sends everything not in skip_push |
probe caps how much data is fetched, but within that cap the whole sync
pipeline runs (images, characteristics, order ingest). abasyn saves no internal
watermark in this mode, so an activated sync on probe keeps trying to fetch the
same goods and documents over and over.
Where new goods land
Shop categories and Abacus groups are independent of each other. A remote
category becomes a TAG_NODES node under the platform-bound tag tree, linked
through CR_TOVAR_NODES. Without landing_group_id, a created good has no group
and is invisible in every group tree in Sklad and Casa.
- Applies to created goods only — a matched good stays where the operator put it and is never moved.
- Skipped for a good that already has a group.
- A non-existent id is logged; goods are still created, just ungrouped.
0is the group-tree root id, which is why it cannot mean a real group.
The shop info pane reports the running count as «Без групи: N» in red.
Page-read characteristics (Prom only)
Prom's seller API has no characteristics endpoint, so the pull additionally
fetches each changed product's public page and parses it — ld+json first, then
the data-qaid="attributes" region. The cost is one extra GET per changed
product, so in steady state this adds no noticeable load, but the first full
pass, if it runs to thousands of pages, can drag on. Treat that first pass like
the first image download: do it once and deliberately, or set
skip_pull: [characteristics] until you are ready. A page that cannot be fetched
holds the catalog watermark rather than leaving the product under-tagged.
5. Turning on writes to the shop
Rehearse first:
rehearse computes every write it would send and logs it at full production
scale. Watch would_push across a few real changes, then promote to live and
verify the result on the marketplace.
On first arming the watermark initialises to the current moment and the run does nothing: changes made before arming are treated as already reconciled. Without this, promoting a connector would sweep the entire history — every good ever deleted in Abacus would be taken off sale in one pass.
Per-channel notes:
- Prom's status list is fixed:
received,delivered,canceled,paidand the loan states. Anything else is skipped with a log entry, but is not an error. WooCommerce accepts any status registered on the site. - Order status is sent with a comparison: the shop's current status is read before writing, so a retry after a transport failure does not duplicate writes, and the echo between pull and push cannot loop.
- Goods are sent without a comparison — writing price and stock is
idempotent and carries our own value, so it goes out in bounded batches of 100
(Prom
products/edit, Wooproducts/batch). - Price is last-writer-wins, implemented in code. The pull writes a remote
price only when the remote
modifiedmarker is newer thanCR_TOVAR_STOREFRONTS.SYNC_TIMESTAMPand the value genuinely differs; a locally repriced good survives the pull and is sent to the shop instead. - Stock is
SUM(TOVAR_AMOUNT)overM_TOVAR_AGGfor all storages — the same number shown in the Залишок column of the shop's items grid. A negative total is sent as 0; an empty price is never sent. If stock is 0, the product is marked as unavailable in the online shop, which is one more reason to test the start of a sync carefully. - Images upload to the shop's media library one at a time, after which the
whole gallery is attached to the product as one ordered list (by
SORT_ORDER, lowest first). A good is only visited when at least one of its images is new or changed.
6. Listing Abacus goods on the shop
CR_TOVAR_STOREFRONTS.IS_LISTED (the У продажу column in the items grid) is
the desired state. The actual state is REMOTE_TOVAR_ID: empty means the
shop does not have the product. The listing feed reconciles the two:
REMOTE_TOVAR_ID |
IS_LISTED |
good TTYPE |
action |
|---|---|---|---|
| empty | 1 | not 3 | list — create on the shop |
| set | 0 | not 3 | delist — hidden from buyers, reversible |
| set | any | 3 | archive — the good was deleted or merged away |
| set | 1 | not 3 | nothing |
| empty | 0 | any | nothing |
So the sequence for putting an Abacus good on sale is: add it to the shop with
Додати товар, set its price in the items grid, tick У продажу, and let a
live run with listing enabled create it on the shop. The ІД у магазині
column fills in once that succeeds.
The two gestures differ. Delisting is reversible: the good disappears from the storefront but stays in the shop's admin, and ticking У продажу brings it back. Archiving is one-way: the shop copy goes to the bin and the shop's own policy decides when it is purged. That is what happens to the duplicate after two goods are merged.
Removing the mapping row (Прибрати товар з вітрини) is a different gesture — it means stop syncing this good here. Nothing is sent to the shop and the remote id is lost. If the product should actually come off sale, clear У продажу first.
7. Doubtful and conflicting mappings
An Abacus good can be accidentally linked to the wrong shop product — for example when the article codes of different goods happen to collide. The Стан синхронізації column in the shop's items grid shows whether a mapping is worth checking:
| Status | Meaning |
|---|---|
| (blank) | checked automatically — the good's name and the shop's name are close enough |
| підтверджено (approved) | the operator confirmed the link with Підтвердити прив'язку; the sync will not touch it again |
| сумнівний (suspect) | the names diverged — the row is shown in red, check it manually |
| конфлікт (conflict) | another shop record claims this same good |
The count of suspect and conflicting rows shows in the Сумнівних прив'язок column of the shops grid and in the shop's info pane; the item row itself is shown in red (archived goods excepted). The Назва у магазині column shows what the shop calls the product — compare it with the local name to see why the mapping was flagged.
The sync never re-points a mapping by itself. When a shop record arrives claiming a good that already has a mapping, the existing link is left untouched, the rival is recorded separately, and the row is flagged as a conflict. No order is lost — lines that found no good are re-matched after every catalog pass.
Fixing it is done with the buttons under the items grid:
- Підтвердити прив'язку marks the current link correct; the sync will not touch it again, even if the names keep diverging.
- Переприв'язати товар offers a choice: move the link to another Abacus good, or — when a rival exists — hand this good to the rival, or attach the rival to a different good. If any order or shipment lines carry this good, you are asked whether to move them along with the link. Each of these actions is one operation in the database: the link, the documents and the audit log always change together.
8. Order ingest
Unless skip_pull names orders, abasyn updates "Онлайн замовлення" and "Онлайн
доставки" after each catalog pass, within the same job. The catalog is synced
first because lines match on CR_TOVAR_STOREFRONTS.REMOTE_TOVAR_ID, then on SKU.
- An unmatched line is kept with an empty
TOVAR_IDand re-matched by the pass that follows the catalog (POST /api/documents/orders/reresolve-lines, reported aslines_reresolved). No manual re-pull is needed. - The buyer collapses to one "online customer" contragent per shop; the real
name, phone and address stay as text in
CLIENT_INFO/SHIPPING_INFO. - Currency resolves through
R_CURRENCIES.EXT_CODE; populate the ISO code once per currency you sell in, orCURRENCY_IDstays empty. - An order the database rejects is not lost: the header is retried once in a
simplified form, flagged in
ONL_ORDER.NOTEand counted as a warning. - An order that finally fails to save is kept in full in
abasyn_state.jsonand re-sent to abrest on the next sync run, with no call to the marketplace. A bookmark cancelled bymax_retriesormax_age_hoursis written to its own JSON file next to the state file, with an error in the log. The job status showsN awaiting retryand returns to OK by itself on success.
Status vocabulary
R_ORDER_STATUS.EXT_CODE scoped by PLATFORM_ID, and it populates itself:
an unknown remote code is created automatically for that platform with a
readable name (checkout-draft → "Checkout draft"). Rename freely — EXT_CODE
stays the key.
- The Prom and WooCommerce vocabularies differ (
receivedvsprocessing); a row with an emptyPLATFORM_IDis a shared fallback, and an exact platform match wins. - A deleted status is treated as a deliberate removal and is not recreated.
- On update the status has two masters: the remote value applies only when it is
newer than
ONL_ORDER.STATUS_SYNC_TS, so an order advanced locally is not rolled back by a lagging pull.
9. Scheduling
The first sync run falls roughly 10 s after startup, so as not to overload the service start.
- Jobs never overlap: if you set a run interval of 600 seconds but the current job does not finish within 10 minutes, the new job is skipped.
- The interval counts from the end of the previous run.
- Runs are serialised across all connectors — one long first pull makes the rest wait. Let it finish before adding more shops.
interval: 1 is safe and formally legitimate, but pointless: every job first
checks the shop and re-reads the category tree, which is always pulled in full,
and marketplaces rate-limit persistent polling, including by answering
429 / Too many requests. abasyn slows down automatically on such an answer.
For normal work use 180–600, for large shops with frequent updates 60.
If seconds genuinely matter, polling is the wrong mechanism: the shop should
notify Abacus by webhook. That is a feature request, not a config value.
interval: 0 together with the Запустити синхронізацію зараз button is a
perfectly workable arrangement — the sync happens on demand only.
10. Both directions are incremental
Each job keeps its own watermarks in abasyn_state.json (the path is resolved
from the standard configuration location — /opt/abacus/etc/,
~/.config/abacus/, else the binary's directory), keyed by the shop id:
| Key | Covers |
|---|---|
catalog_since |
goods |
order_since |
orders |
push_since |
order-status push |
product_push_since |
price, stock and name push |
listing_since |
listing and delisting |
image_push_since |
image upload |
A watermark advances only after a complete pass. A probe cap, a failed or
partial fetch, or a cancellation on service stop holds it, and the next run
re-requests the missed data. Ingest is idempotent, so re-running never creates
duplicates.
- Prom floors
date_modifiedto the hour for goods and to the second for orders, and its filter is inclusive. That is, every good created between, say, 15:00:00 and 15:59:59 carries the same modification time. The connector holds the open bucket and re-fetches it until it closes in UTC, so a later edit sharing the same floored timestamp is not skipped. - WooCommerce uses
modified_afterwithdates_are_gmt=true, exclusive at whole-second precision, so the watermark advances straight to the newestdate_modified_gmt. - Categories are always pulled in full — one small page, with no modification date to filter on.
- Images stay a download-once affair; a good whose image is already downloaded is not read again.
A full re-pull is a watermark deletion: stop abasyn, delete the relevant key
(or the whole abasyn_state.json), start the service again. You need this after
repointing a shop at a fresh database. It is safe, but not quick on a large shop:
the first sync can run for many hours, so plan it in advance.
11. Product images
Abacus fetches photos once, and only for a good that has none.
An image is uploaded when it has never been sent to that shop, or when its content, type or display order changed since it was last sent. Replacing a photo in Abacus therefore replaces it on the shop on the next run. Each upload is recorded separately per shop, so a good listed on two shops keeps a separate media id for each and is never uploaded twice to the same one.
As with every other push, the first run after arming only initialises the watermark and sends nothing: images already in the catalog are assumed to match what the shop has.
Deleting an image is the one case that does not propagate. When you remove a photo in Abacus the local record disappears, but nothing is sent to the shop: the gallery catches up the next time any other image of that good changes. Reordering, by contrast, applies immediately.
12. Product keywords
The seller's keyword list is pulled with each product into the predefined
blob-string tag −104 «Ключові слова» (visible in the good's «Користувацькі
параметри» tab as a one-line preview, with «…» opening the full list). Prom
supplies its own keywords field; WooCommerce supplies tags[] merged with the
Rank Math / Yoast focus keywords where the plugin is installed, de-duplicated
case-insensitively.
An absent or empty list omits the field, and an omitted field leaves the stored
tag alone — a shop that stops reporting keywords never blanks what Abacus holds.
Unless skip_push names products, the tag is written back out.
Troubleshooting
| Symptom | Cause |
|---|---|
| "Службу Abasyn не налаштовано для цього робочого місця" | Profile has no abasyn: section, or enabled: false. |
| "У вас немає токена авторизації" | Services were never activated for the profile. |
| "Спершу вкажіть тип конектора для майданчика" | Platform CONNTYPE is empty. |
| Nothing syncs | No settings block saved (an unconfigured job stays disabled); or Активний unchecked; or interval: 0 with nothing triggering it. |
| 401 from the marketplace | Wrong token or secret. Not a URL problem — tokens key on the shop id. If the shop was deleted and recreated, the token belongs to the old id. |
| 401 from abrest | Stale cached abrest key. POST /api/connectors/reload clears the cache; re-activate the services in Sklad if the token itself is gone. |
| Only 10 goods and 10 orders | mode: probe. Promote to readonly. |
| Goods but no orders | orders listed in skip_pull, or nothing changed since the watermark. |
| A shop-side change did not arrive | Runs are incremental. Clear catalog_since to reconcile. |
| An Abacus-side change did not leave | mode below live, or the entity listed in skip_push, or products skipped (which takes names with it, but no longer stock). Also expected on the first run after promoting to live: the push watermark seeds to "now", so re-touch the record. |
| Orders land on the wrong good | The mapping is confused — check the item's Стан синхронізації column and fix it with Переприв'язати товар (section 7). |
| New goods missing from the group tree | landing_group_id: 0. |
| «Без групи: N» in red | The same goods — their characteristics are offered in every group. Set landing_group_id, and tag_group_id if the tags belong elsewhere. |
| A good stopped being pushed | PUSH_MISS_COUNT reached push_miss_limit — the shop no longer has that product. Clear the mapping or re-list the good. |
| A new product never appeared on Prom | Expected. Prom accepts product creation by import feed only. |
| A photo added in Abacus never appeared on WooCommerce | images listed in skip_push, or mode below live. The job status shows Job-<id>-images with the reason. |
| A photo never appeared on Prom | Expected: Prom's API has no image field. |
| A photo deleted in Abacus is still on the shop | Known limit; it clears when another image of that good changes. |
WooCommerce rejects everything with invalid_username |
The stored password is the user's account password. Generate an application password (Users → Profile → Application Passwords) and re-enter it, step 3. |
| WooCommerce answers 401/403 on image upload only | The WordPress user lacks upload_files. Give it the Shop Manager role. |
| Run failed with "order fetch incomplete; watermark held" | Partial page walk. What was fetched is saved; the next run re-requests the gap. |
| Endpoint returns 404/401 after a service update | abrest registers routes at startup — restart abrest -r. |
Diagnostics
$KEY is the abasyn API key.
One entry per connector: the shop id, then the label, mode, enabled,
last_run, last_stats, last_error fields. Push outcomes appear as
pushed / would_push / skipped_same / skipped_unsupported / not_found / errors,
image runs as candidates / goods / uploaded / would_upload / attached / errors;
catalog stats carry goods_unscoped, order stats lines_reresolved.
# Force an immediate run (the number is the shop id)
curl -H "X-API-KEY: $KEY" -X POST http://localhost:4602/api/jobs/7/run
# Service health; each job appears as worker Job-<id>, and for the write
# direction also Job-<id>-push, Job-<id>-products, Job-<id>-listing, Job-<id>-images
curl -H "X-API-KEY: $KEY" http://localhost:4602/api/status
# Whether abrest can see the database
curl -H "X-API-KEY: $ABREST_KEY" http://localhost:4603/api/status
# -> {... "workers":[{"workerName":"DBWorker","status":"OK", ...}]}
fetched N products, fetched N
orders, job … ok: …). The full abrest contract is at
GET /api/openapi.json.