Skip to content
megapromotingLet's talk

Expertise · Online stores

A store where the catalogue is read in real time, not copied once and forgotten.

We build shops and, on top of them, the layer that is usually missing: the agent that answers the customer reads the price and stock directly from the shop, at the moment of the question, and refuses to invent an unpublished price.

Already builtSe poate verifica din exterior chiar acum, fără să ne întrebi pe noi. Punctul nostru de catalog livrat unui distribuitor răspunde 200 și spune „154 rezultate”; interfața publică a magazinului lui, interogată direct, întoarce antetul `x-wp-total: 154`. Aceeași cifră, două surse independente, verificat pe 06.09.2026. A doua implementare e integrată în platforma de asistenți: serviciul `storeCatalog.service.js` plus trei unelte pe care agentul le poate chema. Rezerva care trebuie spusă: acest al doilea traseu stă pe o ramură de dezvoltare (`feat/store-catalog-global`, `481cfa0`) și nu e încă unificat în trunchi — deci e livrat pentru clienții pe care i-am conectat, nu pornit implicit pentru toți.

Most “catalog integrations” are in fact an export. Someone downloads the products once, pastes them into a file, and from then on the agent talking to the customer reads an old snapshot of the shop. It works perfectly until the first price change or the first sold-out product — and then it gets it wrong with confidence, which is worse than staying silent.

We read the shop at the moment of the question. For WooCommerce shops there is a public interface — Store API — that returns products, prices and availability without keys, without an account, without an installed plugin. The address is `/wp-json/wc/store/v1/products`, we request it with 100 products per page, with a maximum wait time of 20 seconds, and we find out from the response header how many pages there are, instead of guessing. We take the first page on its own, the rest in parallel.

The part that separates a correct integration from one that only looks correct is price translation. Store API does not return “129,90”; it returns a string of digits in minor units and, separately, how many decimals the currency has. Anyone who divides by 100 out of habit silently gets it wrong on any currency that does not have two decimals. Even more important: a zero price does not mean free, it means unpublished. With us, zero becomes “price on request — to be confirmed by colleagues”, and the agent receives explicit instruction not to invent. It can be seen in the public response of the catalogue endpoint, right now.

On top of the catalogue come the other pieces, each with its real state: basket and order submission to the restaurant’s or shop’s management system, payment through the MAIB interface for merchants or through Stripe, abandoned basket recovery, order status tracking with synchronisation every 15 minutes. Where we do not have a working integration — and there are such cases — write exactly which one below.

What it covers

The work, by component

Catalogue read in real time, without keys and without plugin

For WooCommerce we use Store API, the shop’s public interface: `/wp-json/wc/store/v1/products`, 100 products per page, maximum wait time 20 seconds, a custom client identifier in the header and acceptance of code 200 only. We read the number of pages from the `x-wp-totalpages` header, and the total number of products from `x-wp-total` — we do not paginate until we are exposed. The first page is taken on its own, the rest in parallel, with a limit of 30 pages. Nothing is installed in the shop and we are not given merchant keys.

The price translated correctly, and zero treated as unpublished

Store API returns the price as a string in minor units, plus the currency decimal places. We divide by ten to the power of that number, not by 100 fixed — otherwise any currency with a different number of decimal places comes out wrong and nobody notices. Zero becomes `null`, and the response shows “price on request — to be confirmed by colleagues”, together with the explicit instruction given to the agent not to invent and to ask a colleague for confirmation. The price is sent in two forms at once: ready-to-display text for the interface and a number for the logic behind it.

Three tools the agent can call

In the assistants platform, the catalogue is exposed as three built-in tools — keyword search, matching by a device or use case, and fetching a product by identifier. The agent sees them under the names `catalog_cauta`, `catalog_potrivire` and `catalog_produs`, each with its declared parameters. When the shop platform is not the expected one or the address is missing, the tool refuses explicitly instead of returning an empty list that would be interpreted as “we do not have the product”.

Link words do not spoil the result

“Water filter” searched literally matches everything that contains “of”. In the delivered catalogue endpoint and in the search in the voice widget we trim the list of stop words — `and`, `or`, `of`, `from`, `to`, `with`, `on`, `in`, `for` — and, if after trimming nothing useful remains, we fall back to words longer than two letters. Partial matching has a threshold: a product enters the results only if it reaches at least half of the remaining words.

When the shop has no interface, we read the page

Not every shop has a Store API. For the rest we have an extractor that opens listing pages and reads their product cards, with budgets written in code: a maximum of 20 listing pages, 200 products, 24 detail pages, 50,000 HTML characters per page, 5 seconds per request and a 45-second time limit for the whole operation. It reads first the structured product data from the page, then the cards, then the sharing metadata — in that order, because the first source is the one the shop wrote intentionally.

Freshness with written limits, not with hope

The catalogue stays in memory for 10 minutes per shop, and simultaneous requests for the same shop are merged into one, so that ten clients asking at the same moment do not produce ten downloads. On the public catalogue endpoint, the response also carries a cache instruction for the delivery network: fresh for 5 minutes, still served stale for another 10 minutes while it refreshes in the background. The number of results returned is capped at 20, with a default value of 6.

Basket, order and the link with the management system

The basket, order submission and handing it over to the client’s management system are separate functions, not a form. There is also retrieval of abandoned baskets, ordering by conversation and order status tracking, with two scheduled tasks that run every 15 minutes — one synchronises the order result, the other fills in statuses from the management system over a window of two days back and one day forward.

Payments: what is functional and what is not

Functional and in code: the MAIB merchant interface — token, then payment request, with each shop’s credentials read from the database and the function explicitly enabled — and Stripe, with the amount converted into minor units and the collection saved in the records. What is NOT functional, so there are no surprises: Paynet, Netopia and mobilPay have no implementation, and the code explicitly refuses when they are requested. The Moldindconbank route is a skeleton that returns an error without credentials. We take them into the project as work, not as an existing integration.

The agent does not invent when the network goes down

If the shop query fails, the tool does not return an empty list — it returns a message that tells the agent not to formulate an answer about the catalogue. The difference matters: an empty list translates to “we do not have it”, and “we do not have it” said wrongly loses an order just as surely as a wrong price.

What it looks like

The path, step by step.

01

Check the shop before any promise

We query the public interface with a single request, with a maximum wait time of 15 seconds, and read how many products it has from the header. From that response we know whether the live route is possible, how many products there are and how complete the price information is. If the shop does not respond, we say it before the offer, not during implementation.

02

Connecting the catalogue and the first pass through prices

We deliver the connection, plus a pass through products with zero or missing price — they are almost always real products, not errors, and the way the agent handles them is a store decision, not ours. We also deliver the exact text with which the agent refuses to give an unpublished price.

03

The agent’s tools and their limits

We set up search, matching and product retrieval, each with the maximum number of results agreed together. Store-specific synonyms are written here as well — how the customer refers to a product versus how the catalogue names it. Without this step, the search is technically correct and practically useless.

04

The order, payment and link to inventory management

They are built in this order, and each piece is added only after real access to the other end’s system is checked. For payments, we start only from a provider for which we have a working implementation; the rest are treated as new work, with the risk stated earlier, not as a tick in the offer.

05

Handover, with the list of gaps

We hand over the addresses, configurations, limits written in code and the list of what is not covered. For example: the delivered public catalogue endpoint does not have a maximum wait time for the request to the store — if the store responds very slowly, the request is prolonged. This is a real gap, we put it in the handover list and in the repair plan, not in an internal note.

Un centru. În jur, ce intră în el — pe trasee separate.CENTRUL SE SPRIJINĂ PE CE E ÎN JURUL LUI
Magazinul, agentul care răspunde, sistemul de gestiune și furnizorul de plată — patru sisteme separate, cu o singură sursă de adevăr pentru preț: magazinul, citit în momentul întrebării.

The data

What we touch, where it lives, and how long it stays

The questions anyone with a data protection officer would ask — asked here before they do.

Catalogue data
Name, short description, current price and list price, availability, image, category and product address — all read from the store’s public interface, which publishes them anyway to any visitor. We do not copy the store’s database and do not ask for merchant keys. They remain with the client; we read them on request.
Where it is kept and for how long
The catalogue is kept in process memory for 10 minutes per store and disappears on restart — there is no persistent copy of it with us. On the public catalogue endpoint, the distribution network keeps it fresh for another 5 minutes and as an old version for 10 minutes while it is being refreshed. A frozen export, when it is the only technical option, is explicitly marked as a snapshot with a date, not as a live source.
Conversation and order data
What the customer asks and what they order pass through the conversation platform and, where applicable, to the store’s inventory management system. Who has access, how long it is kept and what is deleted are set per project, with the processing register written before launch, not after the first incident.
Payment credentials
Merchant credentials are kept in the platform database, per store, and are read only when the payment function is explicitly enabled for that store. They do not reach the public page and do not pass through the conversation. The merchant account is configured by the account holder, not by us.
What we do not touch
We do not take card data. We do not ask for administrator access to the store to read the catalogue — the interface used is public. When a project really requires privileged access, it is requested separately, with the purpose written down, and is not kept ‘just in case’.

A case

A catalogue of 154 products, read at the moment of the question

The situation

A coffee and water distributor in Chişinău, with a WooCommerce shop and agents who answer customers in text and by phone. The catalogue changes often; a copied list becomes wrong within a few days, and a wrong price said on the phone is not taken back.

What we built

We mounted a separate catalogue endpoint that queries the shop’s public interface: 100 products per page, the number of pages read from the header, the first page fetched on its own and the rest in parallel, cap 30 pages. The price is translated from minor units according to the number of decimals declared by the shop; zero becomes “price on request — confirmed by colleagues”, with a written instruction for the agent not to make anything up. The search strips out linking words and requires a product to match at least half of the remaining words before it appears in results. The response is kept in memory for 10 minutes and in the distribution network for 5 minutes, with another 10 minutes served stale while it refreshes. Voice agents call it as a simple web point, not as a special integration.

What came out

Verified on 06.09.2026, in two independent requests: the catalogue endpoint responds with “154 results” and lists the first product with price and availability, while the second with “price on request — confirmed by colleagues”; when queried directly, the shop interface returns `x-wp-total: 154`. The same figure from two sources that do not know each other — that is the verification, not our statement.

What the case does not say

The delivered catalogue endpoint does not have a maximum waiting time on the request to the shop: if the shop responds very slowly, the request is prolonged instead of failing cleanly. This is a real gap, found on rereading the code, and it is on the repair list — not on the feature list. Separately: the stop-word filter described above is active in the delivered endpoint and in the search from the voice widget; in the version embedded in the platform it has not yet been ported, so there a search with many linking words gives broader results.

Questions

What people ask us before they call

What does “live catalogue” mean and how do I check it is not just talk?

It means the agent reads the shop at the moment of the question. It is checked in two requests, without us: our catalogue endpoint for a distributor responds “154 results”, and the shop’s public interface, queried directly, returns in the header `x-wp-total: 154`. The same number, two independent sources, on the same date. A list copied once cannot do that — it goes out of sync at the first change.

Do I need to give you keys or access to the shop?

For reading the catalogue on WooCommerce, no. Store API is the shop’s public interface: the same data any visitor sees, served in a format readable by a programme, without merchant keys and without an installed plugin. The comment is written in our code itself, so it does not get lost. We only ask for privileged access where the function truly requires it — for example, when sending orders into the management system — and then with a written purpose.

What happens with products without a price?

They are treated as unpublished, not as free. In the shop interface, a missing price comes through as zero, and a naïve integration displays it as “0 MDL”. In our case, zero becomes “price on request — confirmed by colleagues”, and the agent receives the explicit instruction not to invent a figure and to ask a person for confirmation. It can be seen in the public response: the second product in the list appears exactly like that.

How fresh is the price, concretely?

At most 10 minutes old at platform level, per shop, and simultaneous requests for the same shop are merged into a single download. On the public catalogue endpoint, the distribution network serves the fresh version for 5 minutes and the stale version for another 10 minutes while it refreshes in the background. The values are configurable per project; they are written as variables, not hidden.

My shop is not on WooCommerce. What do you do?

It depends on what it exposes. If it has its own interface, we read it like any other. If it has nothing, we have an extractor that opens listing pages and reads product cards, with a budget written in code: 20 pages, 200 products, 5 seconds per request, 45 seconds timeout. It reads the structured data on the page first, then the cards, then the sharing metadata. It is an honest fallback solution, not an equivalent one: a shop that changes its theme can break the extractor, while an interface cannot.

What payments can you integrate right now?

Functional and in code: the MAIB interface for merchants — token retrieval, then payment request, with each shop’s credentials read from the database and the function explicitly enabled — and Stripe, with the amount converted into minor units and the collection saved in the records. What is not functional, said before contract: Paynet, Netopia and mobilPay have no implementation, and the code explicitly refuses when they are requested; the Moldindconbank path is a skeleton that returns an error without credentials. Any of them can be built, but it comes in as new work, with its own risk.

Can the agent invent a product you do not have?

Maybe, if you do not stop it — and that is what we handle. Three barriers: the unpublished price comes with written instructions not to invent; the wrong platform or a missing address makes the tool refuse explicitly, not return an empty list; and a network drop returns a message telling the agent not to form a reply about the catalogue. The difference between an “empty list” and “I could not read” is the difference between a lost order and one postponed with a sentence.

Why does it matter who divides by 100?

Because the interface does not send “129,90”. It sends a string of digits in minor units and, separately, how many decimal places the currency has. Dividing by 100 is correct for currencies with two decimals and silently wrong for the others — nobody gets an error, only bad figures. We divide by ten to the power of the number declared by the shop. It is a line of code, and it is exactly the kind of line that shows whether someone has read the documentation or guessed.

What do you not take responsibility for?

We do not take card data and we do not configure the merchant account instead of the holder. We do not guarantee that a page extractor will withstand a change of shop theme — that is why we prefer the interface when it exists. We do not promise sales growth; what we can show is that the figure stated by the agent matches the figure in the shop, on the same date. And we do not present as an integration something that is, in code, a skeleton: the list above says what they are.

What the statements above rest on (18 sources)
  1. Punctul de catalog livrat răspunde 200 și spune „Din catalogul <magazin> (154 rezultate, primele 2)”, cu al doilea produs marcat „preț la cerere — se confirmă de colegi”https://coffywater-ai.vercel.app/api/catalog?limit=2&scurt=1 · 2026-09-06
  2. Interfața publică a aceluiași magazin întoarce `x-wp-total: 154` și `x-wp-totalpages: 154` — aceeași cifră ca punctul de catalog, din sursă independentăhttps://coffywater.md/wp-json/wc/store/v1/products?per_page=1 · 2026-09-06

16 of them are code and files from our repositories. We don't publish their name or line: together, on a single page, they would describe too precisely how systems that aren't only ours are built. We go through them with you, in the repository, on request — verification stays possible, it just happens in a conversation.

What would you want to work better?

Tell us about your process. Together we decide what is worth building, what we can connect, and how we check the result.

Let's talk