Skip to main content
An app manages the catalogue like a team member: it lists and reads products, creates them, edits their copy, prices, images, options and variants, changes one variant’s price or SKU, and organises collections. Every write runs the dashboard’s own product and collection editors’ checks, and is recorded under your app’s name. No route deletes a product: set active: false to take one off the store. Stock is its own resource (Stock), and so is the buy price (read_costs).

The product

  • type is PHYSICAL or PACK (a bundle of other products); an open string. active is false once the product is taken off the store.
  • Every product has a default variant (isDefault) that follows the product’s own name, image, SKU, barcode and prices. A product with options has one more variant per combination of option values.
  • Deleted variants and options are left out.

List and read

GET /products lists every product of your installation’s stores, active or not, newest first. Filters: storeId, collectionId (the collection’s members), active (true or false), createdSince, updatedSince. A collection membership change does not move updatedAt: listen to products/updated, which names collectionIds.

Images on the storefront

A product’s images and description are loaded by every buyer’s browser on the store, the page that holds the order form. So an app may only write images that are:
  • on the Tassyir CDN: https://cdn.tassyir.io/…; or
  • already shown by the same product or collection (its image, gallery, variant images or description images), so rewriting a product keeps the merchant’s own images.
Anything else is 400 MEDIA_URL_NOT_ALLOWED (field, url). A description is HTML: paragraphs, headings, lists, emphasis, links and CDN images. Refused with 400 HTML_NOT_ALLOWED (field, problems): <script>, <style>, <link>, <meta>, <base>, frames, <object>, <embed>, forms and their fields, <template>, any on… attribute, srcdoc, ping, links that are not http, https, mailto or tel (or that start with //), and inline CSS that can load something (url(, @import, image-set(). null or "" clears a description.

Bring an image onto the CDN

An image from elsewhere (a supplier’s feed, another platform) goes through POST /images first. Tassyir downloads it, keeps it only if it is a JPEG, PNG, GIF or WebP, stores it on its CDN in the merchant’s media library, and answers the URL the catalogue writes accept.
Put url as it is in a product’s image or images, a variant’s image, a collection’s image, or an <img src> of a description. id is the image in the merchant’s media library, where it shows your app as its source. An image already on https://cdn.tassyir.io/ needs no upload.

Limits

A download that was attempted counts, whatever it found. Send an Idempotency-Key per image: a retry with the same key answers the same image without downloading or counting it again. @tassyir/app never retries 429 IMAGE_QUOTA_REACHED, even with retries on: the error comes back at once, and its problem says which quota is full (per, window: day or minute, limit, retryAfter), so your app can wait out a minute window or stop for the day. A raw file upload (multipart) is not offered yet: host the file somewhere first.

Create a product

Any other field (tracksInventory, type, tags, …) is 400 VALIDATION_FAILED. The option and variant rules refuse with their own codes (PRODUCT_VARIANT_COUNT_MISMATCH, PRODUCT_VARIANT_MISSING_COMBINATIONS, PRODUCT_OPTION_DUPLICATE_VALUE, …) and details. The answer is 201 with the product read again, and products/created is sent.

Change a product

PATCH /products/{id} saves the fields you send (the create’s, but not storeId or collectionIds); null clears the nullable ones.
  • options and variants replace the product’s, as the dashboard’s editor saves them: send options with every value and variants with every combination. A variant with an id keeps that variant and changes only the fields you send; one without id is new and needs price; a variant left out is removed. variants alone keeps the options.
  • A product may keep the outside images it already shows.
To change one variant without sending them all, use PATCH /products/{id}/variants/{variantId} with any of name, sku, barcode, price, compareAtPrice, image, hidden:
Two apps changing two variants of one product never overwrite each other. A variant added or removed meanwhile makes the save fail with PRODUCT_VARIANT_COUNT_MISMATCH: read the product again and retry. The default variant follows its product: 409 PRODUCT_DEFAULT_VARIANT, change the product instead.

Collections

A collection holds the products picked by hand, plus those matching its conditions, minus those excluded by hand. Its page lists them in productSort order.
  • parentId nests a collection under another of the same store.
  • productSort: UPDATED, NEWEST, OLDEST, PRICE_ASC, PRICE_DESC, NAME_ASC, NAME_DESC or MANUAL (an open string).
  • Conditions: ruleMatch (ALL or ANY, null without rules) and up to 10 rules: field BRAND or TAG (with valueRefIds), PRICE (valueNumber, and valueNumberMax for BETWEEN, in dinars), ON_SALE, TITLE (valueText); operator IS, IS_NOT, LESS_THAN, MORE_THAN, BETWEEN, CONTAINS, NOT_CONTAINS, STARTS_WITH, IS_TRUE, IS_FALSE.
  • To list its products: GET /products?collectionId=<id>.
  • PATCH /collections/{id}/products: add picks products by hand (and lifts an exclusion), remove takes them out whatever put them there. Up to 250 ids each, products of the collection’s store. Each product that entered or left sends products/updated with collectionIds.
  • DELETE /collections/{id}: its products stay in the catalogue, and its subcollections move up one level.
  • Collections send no webhook of their own.

Errors these routes add

The catalogue’s own codes (options, variants, collections, brands) reach you unchanged, with their details.