Limits and guarantees¶
Product Feed's job is to keep a file that a channel reads about your catalogue, and to keep it correct rather than merely present. This page states what that promises and where the promise stops. Read it before you point a channel at a feed.
At a glance¶
| If you are asking | The short answer |
|---|---|
| Can I change a feed's channel later? | No. The channel cannot be changed after the feed is created. For another channel you add another feed, and Duplicate gives you the same mapping to start from. More |
| Does it send a row per variant, and group them? | One row per OpenCart variant product, grouped with its master's other variants once you map item_group_id from Master product ID. The master itself is not in the group. Ordinary options get no rows: a product with a three-value Size option is one item. More |
| Does it push the feed to the channel? | No. Nothing is pushed anywhere. Google and Meta fetch the file from a URL Product Feed serves; the Microsoft Advertising file you download and upload yourself. More |
| Does it add tax or convert currency? | Only when you choose to. Map a price from Price including tax and your store's own rates are added at your store's own address; tick Convert prices into this currency and your store's own exchange rate is applied. An existing feed does neither until you change it. More |
| Are category suggestions applied for me? | No. Nothing the suggestion heuristic proposes is ever applied by itself. A proposal waits until you accept it. More |
| What if a generation fails? | The previous feed keeps serving, byte for byte. The failure is on the feed list and in the error log. There is no email. More |
| Does it regenerate on its own? | Only if your host calls OpenCart's cron.php, or you add one crontab line of your own. A schedule is only as exact as that cron, and hourly is the finest cycle. More |
| Does switching a feed off stop its URL? | No. Status off stops the schedule regenerating it, and the file it last wrote goes on serving to anybody with the token. New URL or deleting the feed is what stops it. More |
| Is the feed URL protected? | The token is per feed, and it is the whole of the access control. The URL is not a login: anybody holding the token gets your catalogue's public product data. More |
| Does removing it delete my data? | No. Removing Product Feed leaves the generated feed files, its tables and its hourly cron entry behind. More |
| Can the API change anything? | No. It only ever reads, and every route is a GET. More |
What a feed is, and what it is not¶
- A feed is one channel, one store and one language. Selling on Google and Meta is two feeds; selling in Dutch and in English is two more. Everything on the Fields tab is written against the channel's own field set, which is why the channel cannot be changed after the feed is created. For another channel you add another feed, and Duplicate gives you the same mapping to start from.
- One row per product, and variants are grouped only when you map them. A
variant product (the kind made with Add Variant in the product list) is a
product record of its own pointing at its master, so it gets a row of its own
like any other product. Map
item_group_idfrom Master product ID and every variant of one master carries that master's id, which is how a channel reads them as one product in several versions. Ordinary product options have no record of their own (they belong to the product), so a product with a three-value Size option is one item, not three. - The master is not in its own group. It sends no
item_group_id, because it has no colour or size of its own and Google expects every item in a group to differ by one. If your master is only a template nobody buys, leave it out of the feed by name (see which products reach a feed). - A variant's colour or size comes from your store's own options. The source
list has a Variant options group with one entry per option your store has,
and a variant sends the value chosen for it on its master's option: the
value's name for a select or a radio, the names joined with
/for a checkbox, and the text as typed for a text or date option. A master and a product that is not a variant send nothing from it, so the field's fallback applies. An option you delete later resolves empty, and the run carries on. - A variant repeats its master's model and codes unless you changed them.
OpenCart copies the master's model, SKU and barcodes onto each variant it
makes. So
idmapped from Model sends one id for a master and all its variants, which a channel reads as one product sent several times. Product ID is unique per variant. - Only three channels: Google Shopping, Meta and Microsoft Advertising. The fields, tiers and formats of a channel come from that channel's own published specification, so a channel Product Feed does not know is not one you can type in.
- Nothing is pushed anywhere. Google and Meta come and fetch the file from a URL Product Feed serves; the Microsoft Advertising file you download and upload yourself. There is no API integration, no FTP upload and nothing to give Product Feed a channel credential for.
What is in the file¶
- Four prices to choose from, and the mapping is the choice. Price and
Special price are your catalogue numbers, as your product form holds them.
Price including tax and Special price including tax are the same
numbers with the product's tax class applied. There is no tax switch on the
feed: which of the four you map to
priceandsale_priceis the whole decision, and a feed that maps neither tax source sends what it always sent. A store that displays prices including tax and maps Price sends numbers below what a shopper is charged, which the channel will read as your price and may reject the item over. - Tax is worked out at your store's own address, as a guest sees it. That is the feed's store's country and zone and its Use Store Tax Address setting, read from that store's own settings where it has them rather than from the default store's, and the same customer group a special is read for. The arithmetic is OpenCart's: a percentage rate adds that share of the price, a fixed rate adds its amount, and a rate two rules reach is counted once. There is no picker for another country, because that would send a price no shopper on your storefront is shown; a store selling across borders under the EU's one-stop shop sends its home rate. Display Prices With Tax is not read.
- Currency is converted only when you tick Convert prices into this currency. Off, which is where every existing feed starts, the currency on the feed form is the code the number is sent with (every channel refuses a price with no currency code) and nothing more. On, every one of the four prices is multiplied by that currency's value under System → Localisation → Currencies, after tax, which is the order your storefront uses. The rate is whatever that table holds, as its auto-update or a person last wrote it: Product Feed fetches no rates of its own. A currency that is switched off still converts.
- A feed that cannot be converted fails its run, and the previous file keeps serving. If the store has no rate above zero for the feed's currency (it was deleted, say), the run stops with that reason on the feed list and in the error log, rather than sending your catalogue at a rate of zero or in the wrong currency.
- A special price is sent only while it is running, and it is worked out to
the price it comes to: a product 20% off sends the reduced price, not
20.00. It is the special for your default store's default customer group, the one a shopper who is not logged in sees there; a special set only for another customer group is never sent. Special price including tax is taxed on the reduced price. Where no special is running the field resolves empty, so asale_priceis absent rather than equal to the price. A sale price equal to the price is a discount of nothing, which Google reports as a misleading promotion. - Availability is derived, and your own out-of-stock wording is never read. A product whose date available is still in the future is sent as the channel's word for pre-order whatever its quantity says; everything else is in stock above zero and out of stock at or below it. Your store's out-of-stock text is free text in your own language and no channel reads it, so it is offered as a value you can map somewhere yourself and used for nothing automatically.
- Text is text. Every text field has its HTML stripped and its whitespace collapsed, and anything past the channel's character limit is cut, which the Fields tab marks as truncated so you see it before the channel does.
- A value the format refuses is emptied, not guessed at. A price that reads
on requestis not turned into0.00. If the field was required, the product is left out of that feed and counted; if it was not, the field is left out of that product's row. - A field with nothing mapped and no fallback is left out of the file rather than sent empty, because an empty element is a value a channel reads as an answer. A CSV feed is the exception: every column is written for every row, because a row that skipped one would move every value after it into the wrong heading.
- Weight is sent as the number, with no unit. If a channel wants
2.4 kg, the unit has to come from the field's fallback or the field left unmapped. - Images are sent at full size, as absolute addresses on the feed's own store. Channels fetch and cache the files themselves and every one of them prefers the largest version.
What a category mapping does and does not do¶
- The mapping belongs to the store and language, not to the feed. Every feed on that store and language sends what you set, which is what stops you placing the same few hundred categories once per channel. Deleting a feed leaves the mapping where it is.
- Nothing the suggestion heuristic proposes is ever applied by itself. A proposal waits in a table of its own until you accept it; the feed reads the accepted mappings and cannot see a pending one. Suggest looks only at categories nothing is placed on, and running it again replaces the outstanding proposals rather than adding to them.
- The matching is local guesswork on your category names. No API, no key, no network call. It compares the last part of your category path against the taxonomy's, with the rest of the path deciding between the ones that agree, and it proposes nothing at all where no word is shared or where its confidence is below the fixed threshold on the settings page.
- A category with nothing placed on it inherits the nearest mapped ancestor, and the table says so rather than showing a blank. So placing your top level produces a valid feed immediately and refinement is optional. A category with no mapped ancestor sends no category at all.
- A mapping against a category Google has since retired is not sent. Generation keeps climbing to the nearest live ancestor instead; the row shows the path as it read when you chose it, marked as retired, so you can see what it used to mean.
- The taxonomy is the one bundled with the version you installed. It is Google's own published file, and it is the same tree for all three channels: Google and Microsoft Advertising are sent the numeric id, Meta the full path. Refreshing the taxonomy is a Product Feed release, not something the admin can fetch.
- A language Google publishes no taxonomy for is browsed in English. The
bundled files are
en-USandnl-NL; a store language that matches neither says so on the picker.en-gbis not such a language: it matchesen-USon its language alone, so it reads the English tree with no notice. What you pick is a number, and a number means the same category in every language.
Which products reach a feed¶
- Every enabled product in the feed's store, unless a filter says otherwise. Ticking nothing on a filter list restricts nothing.
- Four lists say what may reach a feed, and three say what may not. Categories, brands, stock statuses and a price range are include-lists, and a product has to pass every one you have set something on. Beside them, a category or a brand can be ticked Leave out, and individual products can be named under Leave out these products. Leaving out wins over every include-list: a product in a ticked category is still left out when its brand is. There is no rule engine.
- A category is left out exactly as ticked, the way one is included: leaving out Laptops does not leave out Laptops > Gaming. A product in a left-out category is left out even when it is also in a ticked one.
- Leaving out a brand keeps the products with no brand.
- At most 1,000 products can be left out by name, and saving more is refused. A list that long is a category or a brand, which one Leave out tick says.
- A row both ticked and left out is refused on save. Leaving out would win, so the tick would do nothing; the form asks you to untick one of the two rather than guessing which you meant.
- Stock statuses have no leave-out list, because ticking the other statuses already says the same thing.
- The price filter is your catalogue price, before any special, tax or conversion. A range that moved as specials came and went would drop products out of the feed and back in, which channels read as delisting.
- Products with no brand set are left out by a brand filter, which is what ticking brands means.
- Disabled products are never in a feed, whatever the filters say.
What generation guarantees¶
- A failed generation leaves the previous feed serving, byte for byte. The file is built alongside the live one and renamed over it only when the run finishes, so a channel pulling mid-run gets the previous complete feed or the new complete one and never a half-written file. A bad night does not delist your catalogue.
- A run that reads no products does not replace a feed that had some. When the filters match nothing, or every product in the feed's scope is disabled at the moment the run reads them, as a nightly import that switches products off and on again can leave them, the run fails with read no products and the previous feed keeps serving. The feed list shows the failure until a run reads products again. A feed that has never had any products publishes an empty file, because there is nothing to delist.
- One bad product never fails a run. A product that cannot be represented is left out, counted and recorded with the field that failed. The first 500 of those are recorded per run; the run's own count tells you how many there were. A run that leaves out every product it read is a fault rather than a catalogue, so it fails and the previous feed keeps serving.
- Preview writes nothing, and its refusal count is over the rows it shows. The screen resolves the first 25 products through the same code that writes the feed, so a value on it is the value the channel would receive. But "3 of the 25 rows would be refused" is about those rows and no others. A count over your whole catalogue would mean assembling your whole catalogue, which is generating the feed. No run is opened and the live file is untouched, so previewing a feed a schedule is mid-way through regenerating cannot disturb it.
- The reverse is also true: a run that cannot write its file fails. An unwritable storage directory, a database that went away, a taxonomy file missing from the extension's own files: in each case the run ends, the part-built file is discarded, and the reason is stored on the run and written to your store's error log.
- There is no email. A run that failed is on the feed list and in the error log, and a feed that has gone its own cycle and a further day without a successful generation is called out as overdue there. Nothing is sent to you.
- A run walks the catalogue by product id, in order. So a product added above the position the run has reached is in that file, and one added below it is not; it goes in on the next run. This is deliberate: the alternative skips or duplicates products whenever the catalogue is edited during a run, which on a store that imports at night is every run.
- One run per feed at a time. A scheduled pass that finds a feed already generating skips that feed rather than queueing a second run. A feed is a snapshot of now, so a missed tick is simply skipped.
- A run nobody finished is reclaimed after fifteen minutes. Each slice stamps the run, and a run whose stamp is older than that is marked as stopped without finishing and cleared out of the way. There is no cancel button; that fifteen minutes is what closing the tab costs you.
- The last twenty runs per feed are kept by default, with their rejected-product lists, and older ones are deleted as a new run opens. Runs kept per feed on the settings card changes the number, between 1 and 500, for every feed at once.
Limits on unattended regeneration¶
- Nothing new goes on your server. Product Feed registers one hourly job on
OpenCart's own scheduler, listed under Extensions → Cron Jobs, and reads
off your feeds which of them are due. If your host is not calling OpenCart's
cron.php, nothing regenerates on its own. Nothing scheduled is needed to regenerate a feed from the admin. A host that will not call it is one crontab line of your own instead. - A schedule is only as exact as that cron. An hourly feed on a store whose cron fires twice a day regenerates twice a day. Hourly is the finest cycle a feed can be given, because it is the finest OpenCart's own scheduler understands. For an exact time, use the command line.
- OpenCart 4.1.0.4 cannot run any scheduled task. That release's
cron.phpfails inside OpenCart's own code before any extension is reached, for every extension on the store. It is OpenCart's bug rather than Product Feed's, and no extension can repair it. On that release the command line is the only scheduler you have (one crontab line runs the same pass), and Product Feed's own screen says so, and stops calling a feed overdue for a cron that is not yours to fix. The releases whose own scheduler runs it are on requirements. - A feed is measured from when a run last started, not from when one last finished. So a feed that takes longer than its own cycle is never started on top of itself, and a feed whose runs keep failing is retried on its cycle rather than on every pass.
- Switching a schedule off keeps the cycle, which is how you pause a feed for a fortnight without reconfiguring it.
- Turning the module off stops scheduled regeneration, and nothing else.
The store's cron and the
--cronline below both run the same pass, and it reads that switch. Regenerate now,--feedand--duedo not read it, and the feeds already generated go on serving at their URLs.
Running the scheduled pass yourself¶
--due regenerates the feeds that are due and prints what each one did, which is
what you want at a terminal. The other command is for a crontab:
10 * * * * cd /path/to/store && php extension/product_feed/product_feed.php --cron
--cronruns the scheduled pass, and it is not a second implementation of it. It calls the same job OpenCart's scheduler calls, so what you get is what a working store cron gets: runs recorded as scheduled rather than as yours, the module's own switch honoured, and the pass line in the log on a quiet night as well as a busy one.- It prints nothing when it worked, because cron emails you whatever a job prints and an hourly line is a line nobody reads. What the pass did is in each feed's run history and in the log.
- It exits non-zero only if it could not reach the job at all. A feed that failed is that feed's bad morning rather than the schedule's: the pass carries on, exits clean, leaves the previous file serving, and records the failure.
- Run it hourly. The pass decides for itself which feeds are due, as it does under OpenCart's scheduler, so running it more often than your shortest cycle costs two quick queries and changes nothing.
- Do not run it beside a working OpenCart cron. Both call the same job, and with two schedules over one set of feeds, one of them finds nothing to do. Pick whichever your host actually runs.
- It can only be run from a terminal. A web request to the same file is refused before it reads anything at all, and so is a request to the route behind it.
The feed URL¶
- The URL serves a file and never generates one. A URL that ran a job would be a URL anybody who guessed it could use to run a hundred generations on a catalogue of any size.
- The token is per feed, and it is the whole of the access control. Another
feed's token serves nothing, a feed with no token serves nothing, and every
refusal is the same
Not found. A route that told a caller its feed id was right but its token was wrong would answer questions about which feeds you have. - Microsoft Advertising feeds have no URL and no token at all. That channel takes a file, so its row gets a Download button instead, once the feed has generated. Google and Meta rows carry the URL and no Download button.
- Switching a feed off stops its URL serving. A feed's Status off stops the schedule regenerating it and its URL answers with the same 404 as every other refusal. The file it last wrote stays on disk, and switching the feed back on serves it again. To pause regenerating while the URL keeps serving, turn the schedule off instead. The module's own switch does not stop URLs: it comes back off after every update, and a channel would be delisted for it.
- A new URL takes effect at once. Pressing New URL stops the old one serving immediately, so the channel is being refused until you paste the new one into its dashboard.
- The URL is not a login. It hands out your catalogue's public product data (the same information the channel is being given) to anybody holding the token. Treat it as you would the feed itself.
- Generated files live under your store's storage directory, which a stock
OpenCart keeps inside the web root at
system/storage/. Each file's name carries a long random key of the feed's own, so it cannot be worked out from the feed's number, and New URL gives it a new one, so a file path that leaked with an old URL stops with it. On Apache the folder also carries an.htaccessrefusing a direct fetch. An update renames each feed's file to its new name as it installs, so feeds keep serving through it. - A feed that has never generated is a 404, like every other refusal.
What an update and a removal leave behind¶
- An update keeps your feeds, their tokens and your category mapping. They are records in Product Feed's own tables, and nothing in the extension ever drops a table. An OpenCart update is an uninstall followed by an install, and a version bump that took your mapping with it would be unusable.
- Your store-wide settings survive an update too. Uninstalling deletes the settings, as it does for every OpenCart extension, so Product Feed keeps its own copy and puts it back underneath whatever you configure. The two it does not keep, the module's enabled flag and the Detailed logging window, are on the changelog.
- Deleting a feed deletes its run history with it, rejected-product lists included. Its last generated file stays in the storage directory, but no URL serves it any more.
- Removing Product Feed leaves the generated feed files in your store's storage directory, where nothing in the admin will reach them again. They are yours to delete, and no URL serves them once the extension's files are gone.
- Removing it also leaves its tables and its hourly cron entry. The tables are your data. The cron entry points at a route that is no longer there, and it is deleted under Extensions → Cron Jobs if you would rather it were not listed.
Who can do all this¶
- One permission covers everything Product Feed does in the admin, on the
route
extension/product_feed/module/product_feed. An administrator with it can read every feed's token, download every generated file, and regenerate anything. - Installing it also grants your own user group two screens under Customers: Personal Data, and Remove everything held about a person, which has a permission of its own so that you can withhold it separately. Product Feed's answer on both is that it holds nothing about anybody; see what this holds about a person.
- A generated feed is catalogue data and nothing else. No customer, order or setting of your store's is read by generation or served over a feed URL.
- The command line runs as your store's own default store, with no login.
Anyone who can run PHP in your store's directory can regenerate a feed, and that
is the same person who can already read
config.php.
The API, and what it can read¶
Off for the whole installation until you switch it on, on Product Feed's own
screen under Extensions → Extensions → Modules → Product Feed, and while it
is off every route answers as though this extension had no API at all. Two read
resources: feed is a feed you have configured (its channel, store, language,
whether it is enabled and when it last generated), and run is one generation
attempt, carrying the products it left out on the single fetch of it.
The API is generated from the same declaration the routes
are served from.
- It only ever reads, and every route is a
GET. Nothing regenerates a feed, edits a mapping, accepts a suggestion, rotates a token or deletes a run through it. When to regenerate is a judgement about when your catalogue is quiet, and it stays on your own screen and the command line. - Nothing records that anybody read anything. No read log, no per-credential audit trail, no rate limiting. A read leaves a line in your web server's access log and nothing at all inside OpenCart, so which system pulled my whole feed inventory, and when is a question this API cannot answer and neither can your store.
- The credential is OpenCart's own API user, and it is not scoped. One is enough to read every extension's API on the store and core's own order API besides, across every storefront in the installation. Restrict it by IP address under System → Users → API, and treat it the way you would treat an admin login.
- A feed's URL token is in no answer, under any name. The token is the whole of the access control on a hosted feed, so the value is the authorisation. A read-only credential that could read it would be a way to hand your catalogue to whoever you issued it to, which is the opposite of read-only. It is withheld by name, and a test asserts the value appears nowhere in anything the API emits.
- What you configured is not readable; only what Product Feed produced is. A feed's field mapping and its filter set are not fields on the resource, and your category mappings, the suggestion inbox and Product Feed's own settings are not resources at all. A feed is on the API only because a run has to name the feed it belongs to, and that is as far as the exception goes.
- A run's position in your catalogue is not a field either. A run records the last product id it wrote so the next slice carries on from there. That is a mark inside the generation and not the API's paging cursor; publishing it would put two different things under one word, and promise the way the work is sliced for as long as the version answers.
- A failed run's reason comes back, with your server's paths taken out of it. So an alert can carry why a run failed and not only that it did. Every message Product Feed writes for itself names a file by its name and a directory by what it is for, never by where your store keeps it. What cannot be held to that is a message Product Feed did not write: a database that goes away mid-run reaches this field phrased by its own driver, and what a driver says is not ours to shorten.
- The rejection list is capped, so a capped list is never a complete one. The first 500 rejected products of a run are recorded and the rest are counted: a run reporting four thousand rejections comes back with five hundred of them. The count is the number to reconcile against; the length of the list never is.
- Rejections go when their run goes, deleted with it by the retention above. A run whose rejections you could read yesterday and cannot read today has been retained out, not lost.
- A feed can be read incrementally. A run cannot, and that is deliberate. You can ask for every feed changed since a moment. There is no such filter on a run, because a run's modified stamp is the lease the fifteen-minute reclaim compares against rather than a record of when it last changed meaningfully, so a filter over it would look incremental and drift without ever saying so. Reading runs completely is two calls: walk the collection for the runs you have not seen, then re-read the ones you have that have not finished. A run's row keeps moving after it is created (its state, its three counts and its position all change), so first seen is never final.
- Version 1, and it is the whole promise from its first release. What will not change while it answers, how long it keeps answering once a later version replaces it, and what it deliberately does not offer are on the API promise, written once for every extension rather than restated here.
Language¶
Product Feed says nothing to a shopper. What it publishes is a file a sales channel reads (the titles, descriptions and prices in it are your own catalogue values, taken in whichever language the feed is configured for), and every word Product Feed itself ships is on the screens this documentation describes. So the only language question here is about the screens you work in, and it is answered and counted on the shared language promise page.
Four things, the first of which is often misread:
- Its storefront column on that page reads
—, and that is an absence of shopper text rather than an absence of translation. A feed is read by a channel, not by a customer. There is no string here that a customer ever reads, in any language, so a zero and a full count would both tell you something untrue: one that the work is outstanding, the other that it was finished. Nothing is queued behind that dash. - The screens you work in are translated as far as somebody has read them and no further, and the rest is English. A string a named reviewer has signed off is served in your admin language; a string nobody has read is served in English rather than blank or as its own identifier; and a string whose English has since been reworded goes back to English until it is read again. Which string is in which state is on that page, per language, counted rather than claimed.
- Your category mapping and your feed configuration are yours, per language,
and none of this reaches them. The category you picked for a feed and the
fields you mapped are your store's data, authored against the language you were
working in, and an update leaves them where they are. Editing our
.phpfiles underextension/is a different act: an update replaces those files and takes your change with them. - The Google product taxonomy we ship is per language, and it is deliberately outside everything that page counts. It is a copy of Google's own category tree rather than wording of ours, and it is excluded from the parity check on purpose: nothing here guesses which language of that tree your store wants, and no release is held up because a tree we do not own has not been re-read. So a language whose admin screens are fully translated may still have no taxonomy of its own, and the picker falls back to the English tree. The category codes you assign are the ones Google expects either way, and what changes is only the language you read them in while you are choosing.
Anything not described here should be assumed absent. If a guarantee matters to your store and it is not written on this page, ask before you buy rather than after.