Upsert a product from a shop
const url = 'https://example.com/products/import';const options = { method: 'PUT', headers: { cookie: '__Host-bottle.session_token=<__Host-bottle.session_token>', 'Content-Type': 'application/json' }, body: '{"source":"woocommerce","externalId":"example","externalVariantId":"example","externalSku":"example","name":"example","unitPrice":1,"priceIncludesVat":true,"vatRate":1,"shortCode":"example","updatePrice":true,"brand":"example","description":"example","descriptionHtml":"example","details":[{"detailId":"019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f","value":["019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f"]}]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request PUT \ --url https://example.com/products/import \ --header 'Content-Type: application/json' \ --cookie __Host-bottle.session_token=<__Host-bottle.session_token> \ --data '{ "source": "woocommerce", "externalId": "example", "externalVariantId": "example", "externalSku": "example", "name": "example", "unitPrice": 1, "priceIncludesVat": true, "vatRate": 1, "shortCode": "example", "updatePrice": true, "brand": "example", "description": "example", "descriptionHtml": "example", "details": [ { "detailId": "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f", "value": [ "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f" ] } ] }'Matched on the shop’s own id, so running the same sync twice leaves one product rather than two. The price is written when the product is first seen and left alone afterwards unless updatePrice says otherwise, because Bottle holds the rates agreed with particular customers and a shop knows nothing about them. A listing that comes back un-archives the product. A description (descriptionHtml, sanitised, or plain description) replaces the one here only while nobody has changed that in Bottle since the last import.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”object
Pence, as the shop quotes it. Do not convert: say whether VAT is in it with priceIncludesVat and Bottle keeps the figure exact.
Whether unitPrice includes VAT, which a shop selling to the public usually does. Left out, the business’s own setting applies.
The maker. Fills a blank brand and never overwrites one.
Plain text. Ignored when descriptionHtml is given. On a later import it replaces the description only if nobody has changed it in Bottle since the last import.
The description with formatting, as HTML, sanitised on the server like descriptionHtml on a product. On a later import it replaces the description only if nobody has changed it in Bottle since the last import.
Product details to set, by detail id. Only the details sent change; null clears one. A detail has to be one of the business’s own, not archived, and apply to the kind of product (error: detail_not_for_kind).
object
UUID v7
Example
019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8fResponses
Section titled “Responses”The product, created or updated
object
UUID v7
Pence, exactly as entered. Whether VAT is in this figure is what priceIncludesVat says; the other reading is given beside it for display.
Whether unitPrice includes VAT. False: VAT goes on top when the product is sold. True: unitPrice is the shelf price, and the VAT is worked back out of it on each line.
UnitPrice without VAT, in pence, for display. The same as unitPrice when it excludes VAT or the business is not registered; otherwise derived and rounded. Do not multiply it: amounts are worked out from unitPrice.
UnitPrice with VAT, in pence, for display. The same as unitPrice when it includes VAT or the business is not registered; otherwise derived and rounded.
VAT in basis points: 2000 is 20%, 500 is 5%, 0 is zero rated.
UUID v7
Who pays a deposit or rent where no price list or agreed amount says otherwise. both: Consumer and Trade customers (the default). consumer: everybody who is not a trade customer, signed in or not. trade: trade customers only. Meaningless on anything that is not a charge.
UUID v7
This product’s own UN number, proper shipping name and hazard class, where they differ from its gas type’s. Null follows the gas type.
object
The UN number, four digits without “UN”.
The proper shipping name from the ADR list, as on the cylinder label.
The ADR hazard class: 2.1 flammable gas, 2.2 non-flammable non-toxic gas, 2.2 (5.1) non-flammable gas that is also oxidising, 2.3 toxic gas.
What the product is. The one set on it, or when none has been, read from it: a charge makes it a deposit or hire, a gas type a cylinder, anything else other.
Whether kind was set rather than read from the product.
The description as plain text, with a blank line between paragraphs and a line per list item. Made from descriptionHtml, for search, previews and anywhere HTML cannot go.
The description with its formatting, as sanitised HTML: only p, h2, h3, strong, em, ul, ol, li, br and a. A link has only an http, https or mailto href, rel=“noopener noreferrer nofollow”, and target=“_blank” for a web address. Sanitised again on every read; still treat it as HTML from a third party.
Products picked to show under this one on the shop, in order, up to 8. Empty: the shop shows others from the same category.
On the business’s own shop. Shown there only once the shop is switched on. Never true on an archived product.
Who may buy it on the shop once published. both: everybody (the default). consumer: anybody not signed in for a trade account, so trade customers do not see it. trade: signed-in trade customers only.
On a deposit: what comes back when a cylinder is returned. Null follows the business’s own setting (invoice settings).
object
Which hire a returned cylinder is measured from. Null is latest.
What a trade customer pays, on the shop and on office orders, when no rate has been agreed with them, pence as typed. Null: the list price. A customer’s agreed rate still comes first, and price rules for trade work off this.
What one costs the business, pence without VAT. Optional; read by cost-plus price rules. Never shown to customers.
How many are on the shelf, for things sold on the shop that are not cylinders. Null is not counted: always available. Set and moved with POST /products/{id}/stock, never here. May be below nought when the product keeps selling once none are left.
At or under this count the office is told, and the shop may say low stock. Null is never.
Whether the yard’s cylinder stock keeps a figure for this product. False: the stock list marks it as not counted, rounds plan as though there are always enough, and nothing warns that it is low or has no figure. Only read on a cylinder.
How this product’s stock is recorded. quantity: as a count. individual: each cylinder by its own serial number, once Assets is open and switched on for the business. Only read on a cylinder.
How many months a fill lasts, for a tracked cylinder. Used to suggest the expiry date when stock comes in. Null: no suggestion.
The yard this cylinder is kept at, when it is not wherever a round loads. A round loading at another yard gets a stop at this one to collect what its orders need, before the doors that need it. Null for a cylinder every yard keeps. Only read on a cylinder.
What the shop does when none are left, for this product. Null follows the shop’s own rule (Settings, Shop). stop: off sale. continue: keeps selling, shown as ships when back in stock.
object
UUID v7
What the picture shows, for screen readers and search.
Of the original, in pixels.
Fits inside 200 pixels. WebP.
Fits inside 600 pixels. WebP.
Fits inside 1600 pixels. WebP.
object
The product details it has, in the order the details are listed (GET /product-details). Archived details are left out; their values are kept.
object
UUID v7
Example
{ "id": "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f", "name": "19kg propane", "shortCode": "19", "sku": "PROP-19", "unitPrice": 5400, "unitPriceExVat": 5400, "unitPriceIncVat": 6480, "vatRate": 2000, "charge": "deposit", "chargePeriod": "once", "chargeForProductId": "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f", "chargeForProductIds": [ "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f" ], "chargeAudience": "both", "gasTypeId": "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f", "dangerousGoods": { "unNumber": "1978", "shippingName": "PROPANE", "hazardClass": "2.1" }, "weightKg": 38.5, "volumeLitres": 60, "kind": "cylinder", "brand": "Flogas", "descriptionHtml": "<p>A <strong>13kg</strong> propane refill.</p><ul><li>Same day delivery</li></ul>", "relatedProductIds": [ "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f" ], "shopAudience": "both", "refundRule": "full", "refundCountsFrom": "latest", "stockOnHand": 12, "stockLowAt": 3, "tracking": "quantity", "homeLocationId": "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f", "whenOut": "stop", "images": [ { "id": "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f" } ], "categoryIds": [ "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f" ], "links": [ { "source": "woocommerce", "externalId": "1043" } ], "details": [ { "detailId": "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f", "value": [ "019205d1-6e7a-7c3b-9f6e-3a2b1c0d9e8f" ] } ]}The description is over 50,000 characters as sent or 20,000 once sanitised (error: description_too_long). Nothing was changed.
object
Example
{ "error": "not_found", "message": "Customer not found"}Not signed in
object
Example
{ "error": "not_found", "message": "Customer not found"}Not a member, or without the permission this needs
object
Example
{ "error": "not_found", "message": "Customer not found"}Validation failed
object
object
Example
{ "error": "validation", "issues": [ { "path": "email", "message": "Invalid email" } ]}