Skip to content

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
abasyn:
  enabled: true
  url: http://192.168.1.1:4602/
  • 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_products and upload_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.
  • 0 is 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:

mode: rehearse
skip_push: [listing, images]

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, paid and 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, Woo products/batch).
  • Price is last-writer-wins, implemented in code. The pull writes a remote price only when the remote modified marker is newer than CR_TOVAR_STOREFRONTS.SYNC_TIMESTAMP and the value genuinely differs; a locally repriced good survives the pull and is sent to the shop instead.
  • Stock is SUM(TOVAR_AMOUNT) over M_TOVAR_AGG for 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_ID and re-matched by the pass that follows the catalog (POST /api/documents/orders/reresolve-lines, reported as lines_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, or CURRENCY_ID stays empty.
  • An order the database rejects is not lost: the header is retried once in a simplified form, flagged in ONL_ORDER.NOTE and counted as a warning.
  • An order that finally fails to save is kept in full in abasyn_state.json and re-sent to abrest on the next sync run, with no call to the marketplace. A bookmark cancelled by max_retries or max_age_hours is written to its own JSON file next to the state file, with an error in the log. The job status shows N awaiting retry and 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 (received vs processing); a row with an empty PLATFORM_ID is 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 180600, 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_modified to 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_after with dates_are_gmt=true, exclusive at whole-second precision, so the watermark advances straight to the newest date_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.

curl -H "X-API-KEY: $KEY" http://localhost:4602/api/jobs

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", ...}]}
The abasyn log prints one line per stage (fetched N products, fetched N orders, job … ok: …). The full abrest contract is at GET /api/openapi.json.