למפתחים
חברו את החנות שלכם ל-Hosqi
API קטן לקריאה בלבד או קובץ אחד. Hosqi מייבא את המוצרים, שומר אותם מעודכנים, וה-AI עונה ללקוחות עם המחירים והמלאי האמיתיים שלכם.
החבילה כוללת את המדריך, כלי הבדיקה, סכמות JSON ודוגמת API עובדת.
המדריך זמין באנגלית ובארמנית.
You build a small read-only API on your shop. Hosqi reads it on a schedule, stores the catalog, and answers customers on Instagram, Messenger, WhatsApp and Telegram with your real products, prices and stock. Your shop stays the single source of the data.
Who does what
Keep your shop's business logic. You send its result, and Hosqi says exactly that.
- Owner. In Hosqi: Connections → Connected apps → Add a shop. Enter a name and the address the API will have, press Create a key, copy the key, press Add. The API does not have to exist yet: Hosqi does not call it at this step.
- Owner. Give the developer the key and the Domain token from Settings → For your developer on the shop's page. Use a safe channel, not an ordinary chat.
- Developer. Build the routes and keep the key and the token in the server's settings, not in the code. Check the result with the validator.
- Developer. Put it on production, run the validator on the production address and do the hand checks of section 12.
- Owner. Press Test connection, then Import and index. A shop of 8,000 products took about five minutes.
- Owner. Press Allow AI, then Run the AI test. A workflow that sells from this shop can be published after the test passes.
An API or a file?
| Custom API | Feed file by link (section 13) | |
|---|---|---|
| You build | Two routes, three with /offers | One file at a link |
| Hosqi reads | Only what changed | The whole file |
| Price and stock in a reply | Checked with your shop just before the reply, when you have /offers | As of the last read |
| Good for | Prices and stock that change often | A smaller catalog, or a shop that cannot host routes |
The contract is version 1.1. It only adds prices in other currencies to 1.0 (section 7), so data written for 1.0 stays valid. It has three parts:
| Part | Where |
|---|---|
| This guide | Everything you need to build the API |
openapi.json | The three routes, OpenAPI 3.1 |
| JSON Schema | One file per object, draft 2020-12, in validator/commerce_schemas/1.0/ |
1. Start in five minutes
Run these commands from the folder of this package. Run the reference shop, then run the validator against it. You need Python 3.11 or later.
python sample-api/server.pypip install "jsonschema>=4.26,<5"python validator/commerce_contract.py --api http://127.0.0.1:8787 --allow-local --bearer sample-key --allThe reference shop is sample-api/server.py. It uses only the standard library, so it is easy to read as an example. Its 26 records are in sample-api/catalog.json.
It reports locale_incomplete warnings on purpose, because the examples are shortened and most have English text only. CARPET-CUT reports endpoint_required, because the sample has no /quote and that product goes to a person.
Then point the validator at your own API:
python validator/commerce_contract.py --api https://shop.example/hosqi/v1 --bearer YOUR_KEY --allWithout --all it behaves like Test connection in Hosqi: it reads the manifest, the shop records and two pages of products, and stops at the first failed answer. With --all it reads everything as a sync does, with the waits and retries in section 10, and also checks counts and references and computes your level. Both respect max_rps. Add --json for the full report, and --token with the token Hosqi showed you to test the ownership proof.
The validator is one file and one folder. Keep commerce_contract.py next to commerce_schemas/.
Testing on your own computer. --allow-local lets the validator read a plain http:// address on your computer. Hosqi itself never does: it needs HTTPS on a public address. On a local address the domain proof of section 9 cannot pass, so --token reports ownership_missing there, and that is expected. Product URLs must still be on the domain of store.base_url, so use your real shop domain in the data while you test.
2. What to build
The merchant gives Hosqi a base URL, for example https://shop.example/hosqi/v1.
| Route | Required | Purpose |
|---|---|---|
GET /manifest | Yes | Version, capabilities, counts, last data change |
GET /records | Yes | Catalog records, page by page |
GET /offers | No | Current price and availability for a few variants, just before a reply |
Rules for every route:
- HTTPS on port 443, TLS 1.2 or later, a public address.
- Send
Content-Type: application/json. Hosqi sendsAccept-Encoding: gzip. - Hosqi does not follow redirects. Give the final URL.
- All routes are read-only and safe to repeat.
- Hosqi calls as
Hosqi-Catalog/1.0, one request at a time, withinmax_rps(default 2). A firewall or bot check must let it through. It waits up to 30 seconds for a page, and a run stops after 30 minutes.
3. Authentication
| Method | Request | Use |
|---|---|---|
| Bearer key | Authorization: Bearer <key> | Recommended |
| Named header | X-Api-Key: <key>, the name is your choice | When your shop already works this way |
| Basic | Authorization: Basic <base64> | Older systems |
| Secret URL | A long random path, no header | Only for data that is already public |
Hosqi stores the key sealed and never shows it again. The merchant can replace it at any time.
4. GET /manifest
Hosqi reads this first, on every run.
{
"hcf": "1.1",
"kind": "catalog_api",
"store_name": "Example Shop",
"generated_at": "2026-11-05T09:00:00+04:00",
"data_modified_at": "2026-11-05T08:40:00+04:00",
"counts": {"store": 1, "location": 1, "delivery_profile": 1, "policy": 2, "product": 1240},
"variant_count": 5310,
"capabilities": {"page_size_max": 200, "delta": true, "tombstones": true, "offers": true, "max_rps": 5},
"verification": "hq-verify-7f2a91c0"
}| Field | Required | Notes |
|---|---|---|
hcf, kind | Yes | 1.1 (1.0 is still accepted), catalog_api |
store_name | Yes | Shown to the merchant |
generated_at | Yes | Your clock. Used for delta reads |
data_modified_at | No | Last change to any record. If it has not moved, Hosqi skips the read |
counts | Yes | Records per type at this moment. Count what a full read returns: archived products yes, tombstones no. Always give product, even when it is 0 |
variant_count | No | Shown to the merchant |
capabilities.page_size_max | Yes | Largest limit you accept, 1 to 1,000 |
capabilities.delta | Yes | true if updated_since works |
capabilities.tombstones | Yes | true if deletions appear in delta reads |
capabilities.offers | Yes | true if /offers exists |
capabilities.max_rps | No | Requests per second Hosqi may make. Default 2 |
verification | See section 9 | The token Hosqi shows the merchant |
An ETag is recommended. Base it on the data, not on the body, because generated_at changes on every call. Hosqi sends If-None-Match, and a 304 means nothing changed.
5. GET /records
GET /records?limit=200&cursor=c2tpcD0yMDA&types=product
| Parameter | Notes |
|---|---|
limit | Default 100, up to page_size_max. Hosqi never asks for more. Answer 400 to a value you do not accept, such as one below 1 |
cursor | From the previous page. Absent on the first page. Answer 410 to one you do not recognise, a malformed one included |
types | Comma list of record types. Absent means all. A tombstone counts as its target type. Hosqi uses this only in Test connection, and tolerates an API that ignores it |
updated_since | RFC 3339. Only with capabilities.delta. Inclusive: return records changed at or after this time |
{
"hcf": "1.1",
"generated_at": "2026-11-05T09:00:03+04:00",
"mode": "full",
"records": [
{
"type": "product", "id": "MUG-350-BLUE", "version": 31,
"updated_at": "2026-11-05T08:40:00+04:00",
"kind": "simple", "status": "active", "visibility": "public",
"i18n": {"hy": {"title": "Կապույտ բաժակ, 350 մլ", "url": "https://shop.example/hy/p/mug-350-blue"}},
"variants": [
{
"id": "MUG-350-BLUE", "sku": "MUG-350-BLUE",
"pricing": {"mode": "fixed", "price": {"amount": "4500", "currency": "AMD"}},
"availability": {"state": "in_stock"}
}
]
}
],
"next_cursor": "c2tpcD0yMDA"
}Paging rules:
next_cursorisnullon the last page. An empty page with a cursor is allowed.- The cursor is opaque to Hosqi, and must stay valid for at least 60 minutes. An expired cursor returns
410, and Hosqi restarts the run once. - Keep the order stable, so paging neither skips nor repeats a record: by
idin a full read, byupdated_atthenidin a delta read. Sorting by type first is fine. - Order between record types does not matter. Hosqi resolves references at the end of the run.
- A page is at most 10 MB uncompressed. A record is at most 1 MB. A product has at most 2,500 variant rows.
- An ID may appear on two pages if it changed during the read, and
versiondecides. It must not appear twice on one page.
6. Changes and deletions
Full read. No updated_since. Hosqi reads to the last page and compares totals with counts. They may differ by up to 1% or 5 records, because a catalog can change during a read. A product missing from a completed full read is removed, after a guard: if more than 20% of active products would go, or the read is empty, Hosqi applies nothing and asks the merchant.
Delta read. Hosqi sends updated_since set to the generated_at of its last good run, minus 10 minutes. Return every record changed since then, with mode: "delta". The overlap means a record can arrive twice, and version makes that harmless.
version is an integer that only goes up for each record. The epoch of updated_at works. Hosqi applies a record only if its version is higher than the stored one. Raise it on every change. If you change a price and keep the version, Hosqi skips the record. This also holds for a tombstone: its version must be above the last version of the record it deletes, or Hosqi skips it and keeps selling.
To bring back a deleted ID, send the record with a higher version and restore: true.
Deletions.
| Case | What your API does | What Hosqi does |
|---|---|---|
Product deleted, tombstones=true | Returns a tombstone in delta reads for at least 30 days | Stops selling it at once |
| Product no longer sold | Sends status: "archived" with a higher version | Stops selling it at once |
Product deleted, tombstones=false | Nothing | Sees it at the next full read |
| Variant removed | Sends the whole product without it | Replaces the product |
{"type": "tombstone", "id": "MUG-350-BLUE", "target": "product", "version": 32,
"updated_at": "2026-11-05T10:00:00+04:00", "deleted_at": "2026-11-05T10:00:00+04:00", "reason": "discontinued"}How often Hosqi reads.
| Capabilities | Scheduled run |
|---|---|
delta=false | Full read, skipped when ETag or data_modified_at shows no change |
delta=true, tombstones=true | Delta read, plus a full read at least once a week |
delta=true, tombstones=false | Delta read, plus a full read at least once a day |
Ping (optional)
Hosqi reads your API on its schedule. To have a change read within about a minute, call this address when something changes:
curl -X POST "https://app.hosqi.io/api/platform/catalog/v1/sources/SOURCE_ID/ping" \
-H "Authorization: Bearer YOUR_PING_KEY"The source page in Hosqi shows the address and creates the key, under For developers. The key is shown once. The body is not read, and a ping carries no data: Hosqi still reads the change from your API, only what changed if updated_since works, and everything if it does not.
| Answer | Meaning |
|---|---|
202 | Accepted. Pings are merged, and a source is read this way at most once a minute |
401 | The key is wrong or was replaced |
409 | The source is stopped or disconnected |
503 | Hosqi is applying an update of this source right now. Try again after Retry-After seconds, or let the schedule read the change |
7. Records
Every record has type, id, version and, in this API, updated_at. The record schemas leave updated_at optional because hosted files use them too, and records-page requires it. The schema of each type is the full field list.
type | Schema | Purpose |
|---|---|---|
store | store | Shop facts, markets, customer groups, checkout. Exactly one in a catalog |
location | location | Branch, warehouse, hours |
delivery_profile | delivery-profile | Zones, methods, cut-offs, slots |
policy | policy | Returns, warranty, payment, installments, terms |
product | product | Product with options, variants, rules, add-ons, components |
price_list | price-list | Tiers, member, B2B, market and scheduled prices |
promotion | promotion | Coupons, automatic discounts, free shipping |
inventory | inventory | Fast stock update for a variant and location |
tombstone | tombstone | Deletion |
Value rules.
- Text is UTF-8 and plain. Hosqi strips HTML. A record is at most 32 levels deep.
- Money is a decimal string plus an ISO 4217 code, never a number:
{"amount": "4500", "currency": "AMD"}. A decimal is at most 24 characters. - Time is RFC 3339 with an offset. Local times are
HH:MMand use an IANA zone from the same record, or the store. - Text in several languages uses
i18n, keyed by BCP 47 tags such ashy,ru,en. - IDs are yours, unique per type, stable, and never reused for a different item. Do not put price or stock in an ID.
- Unknown fields are ignored. Put your own data under
x_<vendor>, and the AI will not see it. - An empty
audience: {}means everyone. - Do not send cost prices, supplier data or customer personal data.
store
{
"type": "store", "id": "store", "version": 8, "updated_at": "2026-11-05T08:00:00+04:00",
"name": "Example Shop", "base_url": "https://shop.example", "status": "open", "timezone": "Asia/Yerevan",
"default_locale": "hy", "locales": ["hy", "ru", "en"],
"declared_complete": ["price_list", "promotion", "addons", "delivery_profile", "policy"],
"markets": [
{"id": "am", "countries": ["AM"], "currency": "AMD", "decimals": 0, "rounding": {"mode": "half_up"}, "tax": {"included": true}}
],
"price_currencies": [{"currency": "USD"}, {"currency": "RUB", "decimals": 0}],
"checkout": {"url_template": "https://shop.example/cart/add{?variant,qty}", "carries": ["variant", "qty"]}
}markets[].currencyis the currency the customer pays in. Hosqi never converts currencies. Variant prices use the currency ofbase_market, which is required when there are several markets. A price in another market comes from aprice_list.price_currencies(since1.1) lists other currencies you take payment in and set your own prices in. See Prices in other currencies below.declared_completelists what is complete in your data, including what you do not have. Listingpromotionwith no promotion records means the shop has none.- Product, policy and checkout URLs must be on your domain or in
checkout.allowed_hosts. Image URLs may be on any public HTTPS host. store.endpointsis for live calculations (/quote,/availability), which come later. Leave it out for now. Aliveproduct then goes to a person, with the warningendpoint_required.
product
The product in section 5 is the smallest valid one: type, id, version, kind, status, visibility, i18n with title and url, and variants. Every ID is any text of up to 255 characters.
Every product has at least one variant, even a simple one. status must be active to reach the AI. visibility=members needs audience.
Send short_description and description in i18n for each language. Hosqi stores both, and the AI search reads them, so a customer can find a product by what it is for and not only by its name. short_description is one or two sentences and comes first, and a very long description is cut at the end. Plain text is best, and HTML is stripped.
availability.state is one of in_stock, out_of_stock, preorder (needs available_from), backorder and made_to_order (need lead_time), discontinued, unknown. qty must mean sellable stock. qty_disclosure is hidden unless you set exact or band.
pricing.mode:
mode | Needs | Meaning |
|---|---|---|
fixed | price | A fixed price. Add sale for a scheduled price, or compare_at for a "was" number |
option_sum | base | base plus the price_delta of each chosen option value |
per_measure | rate, per, inputs, min_charge | Price by size or weight. Declare each input in product.live_inputs |
live, on_request | Only your shop knows. The AI hands off to a person | |
bundle | bundle.price_rule | The price comes from the parts |
Options and variants
A product with more than one variant needs options and combinations.
{
"type": "product", "id": "BOOK14", "version": 5, "updated_at": "2026-11-05T08:00:00+04:00",
"kind": "variable", "status": "active", "visibility": "public",
"i18n": {"en": {"title": "Book 14", "url": "https://shop.example/p/book14"}},
"options": [
{"id": "chip", "label": {"en": "Chip"}, "values": [{"id": "base", "label": {"en": "Base"}}, {"id": "pro", "label": {"en": "Pro"}}]},
{"id": "ram", "label": {"en": "Memory"}, "values": [{"id": "16", "label": {"en": "16 GB"}}, {"id": "48", "label": {"en": "48 GB"}}]}
],
"combinations": {
"mode": "rules", "closed": true,
"rules": [{"id": "r1", "when": {"chip": ["base"]}, "require": {"ram": ["16"]}, "why": {"en": "The base chip takes 16 GB only."}}]
},
"variants": [
{"id": "BOOK14-BASE-16", "options": {"chip": "base", "ram": "16"}, "pricing": {"mode": "fixed", "price": {"amount": "759000", "currency": "AMD"}}, "availability": {"state": "in_stock"}},
{"id": "BOOK14-PRO-16", "options": {"chip": "pro", "ram": "16"}, "pricing": {"mode": "fixed", "price": {"amount": "899000", "currency": "AMD"}}, "availability": {"state": "in_stock"}},
{"id": "BOOK14-PRO-48", "options": {"chip": "pro", "ram": "48"}, "pricing": {"mode": "fixed", "price": {"amount": "1099000", "currency": "AMD"}}, "availability": {"state": "in_stock"}}
]
}combinations.mode | Meaning | variants holds |
|---|---|---|
enumerated | Only the listed rows are sold | Every valid combination |
all | Every combination exists | Every combination, or a template row using * |
rules | Every combination allowed by rules exists | Only rows with their own SKU, stock or price. default_variant gives the rest |
live | Too complex to describe | Optional |
closed is required. true means anything not valid under the mode does not exist, so the AI may refuse it. false means the data may be incomplete, so the AI says it will check.
A rule has when (option to values, AND across options, OR inside a list), then at least one of require and forbid, and an optional why the AI can repeat. A value * in a variant matches any value of that option.
In all and rules modes, every valid combination needs a price: a row of its own, a * row, or, in rules mode, default_variant. Otherwise the validator reports combination_uncovered.
delivery_profile, policy, location
{
"type": "delivery_profile", "id": "dp", "version": 3, "updated_at": "2026-11-05T08:00:00+04:00",
"timezone": "Asia/Yerevan",
"methods": [
{"id": "city", "type": "delivery", "zones": [{"country": "AM", "region": "Yerevan"}],
"price": {"amount": "1000", "currency": "AMD"},
"free_over": {"amount": "30000", "currency": "AMD"}, "free_over_basis": "subtotal_before_discounts",
"same_day": true, "cutoff": {"time": "16:00"}},
{"id": "pickup", "type": "pickup", "location_ids": ["main"], "price": {"amount": "0", "currency": "AMD"}}
]
}Each method has zones or location_ids, and exactly one of price, price_bands, quote_required.
{
"type": "policy", "id": "returns", "version": 2, "updated_at": "2026-11-05T08:00:00+04:00",
"policy_type": "returns", "approved_at": "2026-11-01T10:00:00+04:00",
"i18n": {"en": {"summary": "Unused items can be returned within 14 days."}},
"returns": {"accepted": true, "window_days": 14, "who_pays": "customer"}
}i18n.summary is the text the AI may repeat. approved_at says when the merchant approved it.
{
"type": "location", "id": "main", "version": 1, "updated_at": "2026-11-05T08:00:00+04:00",
"name": "Main shop", "address": "1 Example Street, Yerevan", "kinds": ["pickup", "shop"],
"hours": [{"days": ["mon", "tue", "wed", "thu", "fri", "sat"], "from": "10:00", "to": "20:00"}]
}An availability without location_id is your answer for the whole shop, however many locations you have. Hosqi does not add it up or work it out again, so send what your own site would tell a customer.
Branches, pickup and units
Keep your business logic. Send its result, and Hosqi says exactly that.
Stock by branch. availability.state stays your overall answer. availability.locations says where the product is, in the product record and in /offers. This product shows all three things in this section:
{
"type": "product", "id": "PIPE-12", "version": 1762470000123, "updated_at": "2026-11-05T08:00:00+04:00",
"kind": "simple", "status": "active", "visibility": "public", "delivery_profile_id": "pickup-only",
"i18n": {"en": {"title": "Copper pipe", "url": "https://shop.example/p/pipe-12"}},
"variants": [{
"id": "PIPE-12", "pricing": {"mode": "fixed", "price": {"amount": "3200", "currency": "AMD"}},
"sale_unit": {"unit": "ft", "display": "per ft", "step": "10", "min": "10"},
"availability": {"state": "in_stock", "locations": [
{"location_id": "main", "state": "in_stock"},
{"location_id": "east", "state": "out_of_stock"}
]}
}]
}- Each entry needs
location_idandstate.qty,qty_disclosure,lead_time,available_fromandas_ofare optional. - A product sent with
locationsreplaces the branches Hosqi holds, so send all of them, including those at zero. inventoryrecords withlocation_idare the other way. Each branch then has its ownversion, and only a record newer than the product changes the overall state. If your productversionalready follows stock, uselocations.
Pickup-only products. A product whose delivery_profile_id points to a profile with only pickup methods is never offered for delivery. Replies name the branches from the method's location_ids that have it, as pickup only: East, Main. The warning pickup_place_missing means the product is in stock, but in no branch of that method.
Units. A price with nothing after it reads as the price of one piece. For a product sold by a measure, add sale_unit to the variant, as above, and keep the title as it is on your site.
unitis a code.ft,m,yd,kg,lb,landm2are translated in replies, andpiece,pc,pcs,each,ea,itemandunitshow nothing.displayis your own words, used in your default language and for any other code.stepandminsay what can be ordered.qtymust use the same unit as the price, or leave it out.
No public price. With on_request, live or disclosure: "verified_only", a reply says price on request, and a customer who needs that price goes to a person.
Prices in other currencies
Some shops sell in one country but take several currencies, and set a price in each themselves. Version 1.1 carries those prices next to the main one:
{
"type": "product", "id": "PHONE-6", "version": 4, "updated_at": "2026-11-09T08:00:00+04:00",
"kind": "simple", "status": "active", "visibility": "public",
"i18n": {"en": {"title": "Northline Phone 6, 128 GB", "url": "https://shop.example/p/phone-6"}},
"variants": [{
"id": "PHONE-6", "availability": {"state": "in_stock"},
"pricing": {
"mode": "fixed",
"price": {"amount": "189000", "currency": "AMD"},
"currency_prices": {
"USD": {"price": {"amount": "489", "currency": "USD"}},
"RUB": {"price": {"amount": "39900", "currency": "RUB"},
"sale": {"price": {"amount": "37900", "currency": "RUB"}, "valid_to": "2026-11-30T20:00:00Z"}}
}
}
}]
}- Declare the currencies once in
store.price_currencies, in the order you prefer, as in the store above. List only currencies you take payment in, at most 10, and never the currency of a market. currency_pricesgives each declared currency aprice, and an optionalsaleandcompare_at, all in that currency. A sale in one currency does not touch the others.- The main
pricestays in the market currency and stays required. Leave a currency out when the product has no price in it. Hosqi never converts, so that currency is not shown for this product. - It is the final price of the variant, used with
modefixedoroption_sum. With any other mode it is ignored, with the warningcurrency_prices_ignored. decimalsdefaults to the ISO 4217 minor unit.basisisfixed(the default) when you set the price, orshop_ratewhen your shop's system computed it from its own exchange rate.- The owner chooses in Hosqi which of these prices the AI shows, how many at once, and which comes first.
- A key that is not in
price_currencies, or an amount in another currency inside it, iscurrency_not_in_market. A repeated currency, or a market's currency, inprice_currenciesisprice_currency_invalid.
Price lists, promotions, add-ons, bundles
All four appear in the examples in sample-api/catalog.json, and the schemas give every field.
- A
price_listentry is a tier whenmin_qtyis above 1. Tiers needtier_mode(all_unitsorgraduated) andtier_scope(variantorproduct).disclosure: "verified_only"keeps the numbers away from unverified contacts. - A
promotiongives the text the AI may say. Hosqi does not compute discounts. A code needscode_disclosure. addonschange an order without being a variant. Each has apricewith akind, andcarrysays how the choice reaches your shop.text,number,dateandfileadd-ons needvalidation, even if it is empty.carry: "link"needsaddonsorpersonalisationincheckout.carries.- A
bundleproduct listscomponents. Bundles are one level deep.
8. GET /offers
Optional. With it, Hosqi can confirm a price or stock figure at the moment of a reply. Without it, Hosqi relies on the last sync and its age.
GET /offers?variant_ids=MUG-350-BLUE,BOOK14-PRO-48&market=am
{
"hcf": "1.1",
"generated_at": "2026-11-05T12:00:00+04:00",
"offers": [
{"variant_id": "MUG-350-BLUE", "product_id": "MUG-350-BLUE", "status": "ok",
"pricing": {"mode": "fixed", "price": {"amount": "4500", "currency": "AMD"}},
"availability": {"state": "in_stock"}},
{"variant_id": "BOOK14-PRO-48", "status": "not_found"}
]
}- Up to 20 IDs. Answer
400to more.marketis required when the store has more than one market, and Hosqi leaves it out when there is one. pricingandavailabilityare required withok, in the same shape as the catalog. Anoption_sumprice staysoption_sum.pricingincludescurrency_priceswhen the variant has them, so one call checks every currency.- The reply needs
Cache-Control: no-store. - Answer within 2,500 ms. Hosqi never holds a customer's reply longer than that.
not_foundmeans Hosqi stops offering that variant.
9. Proving the domain is yours
Hosqi shows the merchant a token. Any one of these proves ownership:
/manifestreturns the token inverification, and the API host is on the same domain asstore.base_url.- A DNS TXT record on the shop's domain, with the value
hosqi-verification=<token>. - A
<meta name="hosqi-verification">tag on the home page.
The owner finds the token on the shop's page, under Settings → For your developer → Domain token.
10. How Hosqi treats your answers
| HTTP | Hosqi does |
|---|---|
| 200 | Reads the body |
| 304 | Nothing changed, the run ends as a success |
| 400, 401, 403, 404 | Stops the run, tells the merchant |
410 on /records | Restarts once |
| 429, 503 | Waits for Retry-After, up to 5 minutes |
| Other 5xx, timeout | Retries the same page after 2 s, 10 s and 60 s, then stops the run |
A stopped run never changes the published catalog. You may describe an error with application/problem+json, and Hosqi shows its title to the merchant.
11. Validation
The validator is the code Hosqi runs. Severity decides what happens:
| Severity | Effect |
|---|---|
fatal | Nothing from this run is applied |
error | The record is rejected, and the previous version stays. If more than 2% of records are rejected, and more than one, the run becomes fatal (too_many_rejected). A delta read counts against the size of the catalog, not of the read |
warning | The record is accepted. Fix it when you can |
info | For your attention only |
Each issue has code, severity, record_type, record_id, pointer, message and hint. The commonest ones:
| Code | Wrong | Right |
|---|---|---|
invalid_money | "price": 4500 | "price": {"amount": "4500", "currency": "AMD"} |
missing_required | "i18n": {"hy": {"title": "Բաժակ"}} | "i18n": {"hy": {"title": "Բաժակ", "url": "https://shop.example/hy/p/mug"}} |
option_value_unknown | Option size has s, m, and a variant says "options": {"size": "xl"} | Add xl to options, or fix the variant |
duplicate_combination | Two variants with {"size": "m", "color": "red"} | One variant for each combination |
combination_not_allowed | A variant breaks a rule | Fix the variant or the rule |
url_off_domain | "url": "https://cdn-shop.net/p/mug" on a source for shop.example | Use your domain, or add the host to checkout.allowed_hosts |
html_received | /records returns a login page | Exempt the route from the login or firewall rule |
pagination_loop | next_cursor is the same on every page | Return the next position, and null on the last page |
count_mismatch | counts.product is 1,240 but 300 arrive | Fix the query, or return the true count |
Level. Hosqi computes a level. It is never claimed.
| Level | Your data has | The AI can |
|---|---|---|
| L0 | A fatal problem, no store, or no active product | Nothing from this catalog |
| L1 Basic | store, products with id, title, URL, price, availability | Answer simple questions, send a product link |
| L2 Variants | Clean options, variants, combinations | Guide option choice, refuse invalid combinations |
| L3 Commerce | delivery_profile, policy (returns and payment, approved in the last 12 months), declared_complete | Answer delivery, returns and policy questions |
The validator's level comes from the data alone. Hosqi's live checks, such as comparing links and prices with your site, can lower it.
What the validator cannot see. It tries updated_since with an old and a current time, and it measures /offers, but it cannot delete your data or change your prices. Test these by hand: a tombstone appears in a delta read, version rises on every change, a 304 comes back when nothing changed, and a long catalog pages without skipping a record.
12. Before you connect
/manifestand/recordsanswer on HTTPS, port 443, with your key.- The validator with
--allreports nofataland noerror. versiongoes up on every change, for every record, and a tombstone is above the record it deletes.- After you change a price and delete a product, a delta read returns both.
- A firewall or bot check lets
Hosqi-Catalog/1.0through. - You know how the merchant will prove the domain (section 9).
- A change of stock at one branch reaches a delta read with a higher
version. - A value that comes from settings, such as
free_over, raises theversionof its record when it changes. - The key and the domain token are in the server's settings, not in the code.
Then tell the owner. The owner presses Test connection, then Import and index. If something is refused, Hosqi shows its code and how to fix it.
13. A file instead of an API
When a small API is too much, the shop publishes one file at a link, and Hosqi reads it on a schedule. In Hosqi choose Feed file by link.
- JSON Lines: UTF-8, one record per line,
.jsonlor.jsonl.gz, on HTTPS. - The first line is a
feedrecord.source_idis the Source ID under For your developer. - Each next line is a record of section 7, the same as
/recordsreturns. - Raise
sequencewith each new file, and give incountshow many records of each type follow. - Send an
ETag. Hosqi sendsIf-None-Match, and a304means nothing changed. - Prove the domain with DNS or the meta tag of section 9.
{"type": "feed", "id": "feed", "version": 1762470000123, "hcf": "1.1", "source_id": "YOUR_SOURCE_ID", "mode": "full", "generated_at": "2026-11-05T08:00:00+04:00", "sequence": 1, "counts": {"store": 1, "product": 2500}}Check the file before you publish it:
python validator/commerce_contract.py --file feed.jsonl14. Mistakes we have seen
versionfollows only the product table. A price or stock change never reaches Hosqi. Use the latest change of every table that feeds the record.versionfollows a job that rewrites rows on every run. Every product looks changed on every read. Follow the real change, not the time of the job.- A value from settings changes, such as
free_over, andversionstays. Hosqi applies only a higherversion, also on a full read, so replies keep the old rule. Raise theversionof thatdelivery_profile,storeorlocation. - Only the main branch is sent. Products in other branches read as out of stock. Send your overall
state, and every branch inlocations. location.namerepeats the shop's name. Replies run long, and branches are cut to three and+1. Use a short name, such asSun Valley.- The unit is in the title, or
qtyis in pieces while the price is per foot. The reply reads wrong. Keep the title as on your site, addsale_unit, and sendqtyin the unit of the price or not at all. - A price the public cannot see is sent as
fixed. Customers read a price your site hides. Useon_request. - A product
urlis a placeholder, such as/product/pending-123. The customer gets a page that does not help. Send the page the product really has. - A login, firewall or bot check answers Hosqi.
html_received, or the read stops. LetHosqi-Catalog/1.0through, and give the final address, because Hosqi does not follow redirects.