# Get an access token Source: https://developers.tassyir.io/api-reference/admin/authentication/get-an-access-token /openapi/openapi.2026-10.json post /apps/oauth/token Three grants: the token exchange of an embedded page's ID token (an offline token, or an online one bound to the dashboard user), an install code, and a refresh token (rotated on every use; a spent one presented after 60 seconds revokes its whole family). Client authentication is HTTP Basic only. Errors are OAuth's `{ error, error_description }`, not problem+json. Limits: 300 requests a minute per client id, 30 failed authentications a minute per IP (429 `slow_down`). # ID-token public keys (JWKS) Source: https://developers.tassyir.io/api-reference/admin/authentication/id-token-public-keys-jwks /openapi/openapi.2026-10.json get /apps/.well-known/jwks.json Verify the ID token your embedded page receives: pick the key by `kid`, require `alg: EdDSA`, `iss = https://api.tassyir.io/apps`, `aud = `, and `exp`/`nbf` with at most 30 s of clock skew. # Webhook public keys Source: https://developers.tassyir.io/api-reference/admin/authentication/webhook-public-keys /openapi/openapi.2026-10.json get /apps/webhook-keys Verify a delivery: the signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`; accept it when any `v1a,` signature verifies with any key here and the timestamp is within 5 minutes. # Export a whole list Source: https://developers.tassyir.io/api-reference/admin/bulk-exports/export-a-whole-list /openapi/openapi.2026-10.json post /apps/2026-10/bulk/exports Starts an export of every row the list route of `resource` returns (with these `filters`, the list's own), as one JSON Lines file: one resource per line, exactly as the list returns it, newest first. It runs in the background: read `GET /bulk/exports/{id}` or subscribe to `bulk/finished`; once `completed`, download the file from `url` (7 days). 1 export queued or running per installation, 5 asked for per business (every app together) in any hour. Send an `Idempotency-Key`: a retry then answers the same export. The scope depends on the list exported: `orders` needs the `read_orders` scope (permission `orders:read`); `products` needs the `read_products` scope (permission `products:read`); `customers` needs the `read_customers` scope (permission `customers:read`). # Get an export Source: https://developers.tassyir.io/api-reference/admin/bulk-exports/get-an-export /openapi/openapi.2026-10.json get /apps/2026-10/bulk/exports/{id} Where it stands, and its download `url` once `completed`. Poll it at most every few seconds, or subscribe to `bulk/finished`. The scope depends on the list exported: `orders` needs the `read_orders` scope (permission `orders:read`); `products` needs the `read_products` scope (permission `products:read`); `customers` needs the `read_customers` scope (permission `customers:read`). # List the installation's exports Source: https://developers.tassyir.io/api-reference/admin/bulk-exports/list-the-installations-exports /openapi/openapi.2026-10.json get /apps/2026-10/bulk/exports Newest first, kept 30 days. Only the lists whose read permission the caller holds; an online token sees only the exports its own user asked for. The scope depends on the list exported: `orders` needs the `read_orders` scope (permission `orders:read`); `products` needs the `read_products` scope (permission `products:read`); `customers` needs the `read_customers` scope (permission `customers:read`). # The business the app is installed on Source: https://developers.tassyir.io/api-reference/admin/business/the-business-the-app-is-installed-on /openapi/openapi.2026-10.json get /apps/2026-10/business Every installation may read it, whatever its scopes: the id, name, currency, country and time zone, nothing else. Every installation may call it, whatever its scopes. # Change a collection's members Source: https://developers.tassyir.io/api-reference/admin/collections/change-a-collections-members /openapi/openapi.2026-10.json patch /apps/2026-10/collections/{id}/products `add` picks products by hand (and lifts an exclusion); `remove` takes them out whatever put them there. Needs the `write_products` scope (permission `categories:update`). # Create a collection Source: https://developers.tassyir.io/api-reference/admin/collections/create-a-collection /openapi/openapi.2026-10.json post /apps/2026-10/collections Needs the `write_products` scope (permission `categories:create`). # Delete a collection Source: https://developers.tassyir.io/api-reference/admin/collections/delete-a-collection /openapi/openapi.2026-10.json delete /apps/2026-10/collections/{id} Its products leave it (they stay in the catalogue); its subcollections move up one level. Needs the `write_products` scope (permission `categories:delete`). # Get a collection Source: https://developers.tassyir.io/api-reference/admin/collections/get-a-collection /openapi/openapi.2026-10.json get /apps/2026-10/collections/{id} Needs the `read_products` scope (permission `categories:read`). # List collections Source: https://developers.tassyir.io/api-reference/admin/collections/list-collections /openapi/openapi.2026-10.json get /apps/2026-10/collections Needs the `read_products` scope (permission `categories:read`). # Update a collection Source: https://developers.tassyir.io/api-reference/admin/collections/update-a-collection /openapi/openapi.2026-10.json patch /apps/2026-10/collections/{id} Needs the `write_products` scope (permission `categories:update`). # Get a customer Source: https://developers.tassyir.io/api-reference/admin/customers/get-a-customer /openapi/openapi.2026-10.json get /apps/2026-10/customers/{id} Needs the `read_customers` scope (permission `customers:read`). # List customers Source: https://developers.tassyir.io/api-reference/admin/customers/list-customers /openapi/openapi.2026-10.json get /apps/2026-10/customers The buyers with a counted order in the installation's stores. `name` and `phone` only with `read_customer_contact`. Needs the `read_customers` scope (permission `customers:read`). # Delete a value Source: https://developers.tassyir.io/api-reference/admin/data/delete-a-value /openapi/openapi.2026-10.json delete /apps/2026-10/data/{ownerType}/{ownerId}/{key} Works for a key the version no longer declares too: that is how an app frees room after dropping a key. The scope depends on the owner type: `business` needs none; `store` needs the `read_stores` scope (permission `stores:read`); `product` needs the `read_products` scope (permission `products:read`); `order` needs the `read_orders` scope (permission `orders:read`); `customer` needs the `read_customers` scope (permission `customers:read`). # Get a value Source: https://developers.tassyir.io/api-reference/admin/data/get-a-value /openapi/openapi.2026-10.json get /apps/2026-10/data/{ownerType}/{ownerId}/{key} Read whatever the manifest says now: a key the installation's version no longer declares is still read (`declared: false`). The scope depends on the owner type: `business` needs none; `store` needs the `read_stores` scope (permission `stores:read`); `product` needs the `read_products` scope (permission `products:read`); `order` needs the `read_orders` scope (permission `orders:read`); `customer` needs the `read_customers` scope (permission `customers:read`). # List the app's values Source: https://developers.tassyir.io/api-reference/admin/data/list-the-apps-values /openapi/openapi.2026-10.json get /apps/2026-10/data The values this installation keeps, newest first, of the owner types its scopes may read. A value whose owner it can no longer read is left out, so a page can hold fewer than `limit` values while `nextCursor` is set: keep reading until it is null. `ownerId` needs `ownerType`. The scope depends on the owner type: `business` needs none; `store` needs the `read_stores` scope (permission `stores:read`); `product` needs the `read_products` scope (permission `products:read`); `order` needs the `read_orders` scope (permission `orders:read`); `customer` needs the `read_customers` scope (permission `customers:read`). # Write a value Source: https://developers.tassyir.io/api-reference/admin/data/write-a-value /openapi/openapi.2026-10.json put /apps/2026-10/data/{ownerType}/{ownerId}/{key} Writes the whole value. The key must be declared for the owner type by the installation's version; the value must be of the declared type and at most 16,384 bytes of compact JSON; `visibility` comes from the declaration. An installation holds at most 50,000 values: past that a NEW key is refused, an existing one is still rewritten. The scope depends on the owner type: `business` needs none; `store` needs the `read_stores` scope (permission `stores:read`); `product` needs the `read_products` scope (permission `products:read`); `order` needs the `read_orders` scope (permission `orders:read`); `customer` needs the `read_customers` scope (permission `customers:read`). # Get a delivery agent Source: https://developers.tassyir.io/api-reference/admin/delivery/get-a-delivery-agent /openapi/openapi.2026-10.json get /apps/2026-10/delivery-agents/{id} Needs the `read_delivery` scope (permission `delivery-agents:read`). # Get a delivery fee group Source: https://developers.tassyir.io/api-reference/admin/delivery/get-a-delivery-fee-group /openapi/openapi.2026-10.json get /apps/2026-10/delivery-fee-groups/{id} With its fees, one per wilaya. Needs the `read_delivery` scope (permission `delivery-fees:read`). # List delivery agents Source: https://developers.tassyir.io/api-reference/admin/delivery/list-delivery-agents /openapi/openapi.2026-10.json get /apps/2026-10/delivery-agents The carriers and private couriers the business ships with. Credentials are never returned. Needs the `read_delivery` scope (permission `delivery-agents:read`). # List delivery fee groups Source: https://developers.tassyir.io/api-reference/admin/delivery/list-delivery-fee-groups /openapi/openapi.2026-10.json get /apps/2026-10/delivery-fee-groups Every group at once, without its fees (read one group for them). Needs the `read_delivery` scope (permission `delivery-fees:read`). # Upload an image from an address Source: https://developers.tassyir.io/api-reference/admin/images/upload-an-image-from-an-address /openapi/openapi.2026-10.json post /apps/2026-10/images Downloads the image at `url` and stores it on the Tassyir CDN; the answer's `url` is what product and collection writes accept. https on port 443 only, a public host name; a JPEG, PNG, GIF or WebP image (read from its bytes, never its name or `Content-Type`; SVG refused) of at most 10,000,000 bytes, 12,000 pixels on a side and 100 megapixels. Quota: 1,000 a UTC day per installation, 2,000 per business (every app together; a dev store 100 and 200), 30 a minute per installation. Send an `Idempotency-Key`: a retry then answers the same image without uploading it again. Needs the `write_products` scope (permission `products:create`). # Correct a stock count Source: https://developers.tassyir.io/api-reference/admin/inventory/correct-a-stock-count /openapi/openapi.2026-10.json post /apps/2026-10/inventory-levels/{id}/adjust The dashboard's "correct the count, and say why". Never below zero at the location. Needs the `write_inventory` scope (permission `inventory:update`). # Get a stock level Source: https://developers.tassyir.io/api-reference/admin/inventory/get-a-stock-level /openapi/openapi.2026-10.json get /apps/2026-10/inventory-levels/{id} Needs the `read_inventory` scope (permission `inventory:read`). # List stock levels Source: https://developers.tassyir.io/api-reference/admin/inventory/list-stock-levels /openapi/openapi.2026-10.json get /apps/2026-10/inventory-levels One per tracked variant. `unitCost` only for a caller holding `read_costs`. Needs the `read_inventory` scope (permission `inventory:read`). # Add a code to an offer Source: https://developers.tassyir.io/api-reference/admin/offers/add-a-code-to-an-offer /openapi/openapi.2026-10.json post /apps/2026-10/offers/{id}/codes One code for a `code` offer. Codes are unique across the business (any case, disabled ones included): 409 `OFFER_CODE_TAKEN`. An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `write_offers` scope (permission `offers:update`). # Add up to 250 codes to an offer Source: https://developers.tassyir.io/api-reference/admin/offers/add-up-to-250-codes-to-an-offer /openapi/openapi.2026-10.json post /apps/2026-10/offers/{id}/codes/bulk One call, at most 250 codes (one per influencer, say). Partial by design: a code with a bad format, a repeat inside the request or one the business already has is reported for THAT entry in `results` and the others are created; the call is 200 whatever the mix. An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `write_offers` scope (permission `offers:update`). # Create an offer Source: https://developers.tassyir.io/api-reference/admin/offers/create-an-offer /openapi/openapi.2026-10.json post /apps/2026-10/offers Always a DRAFT, owned by the calling installation: publish it with `POST /offers/{id}/publish`. The body is the dashboard's create body (`kind`, `method`, `title`, `label`, `config`, `combinesWith`, `eligibility`, limits, dates, `targets`, and up to 250 `codes` for a `code` offer) with the store in the body: `config` is the rule exactly as the Offers page stores it, not the shape `GET /offers` answers. Validated by the same rules as the dashboard (`400 OFFER_INVALID` lists `errors` with a `field` and a `code` each). An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `write_offers` scope (permission `offers:create`). # Delete or archive an offer Source: https://developers.tassyir.io/api-reference/admin/offers/delete-or-archive-an-offer /openapi/openapi.2026-10.json delete /apps/2026-10/offers/{id} An offer nobody ever redeemed is deleted with its codes and targets (`deleted: true`, `offers/deleted`). One with any redemption keeps its history and is archived instead (`archived: true`, status `ended`, `offers/updated`); archiving again changes nothing. An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `write_offers` scope (permission `offers:delete`). # Disable a code Source: https://developers.tassyir.io/api-reference/admin/offers/disable-a-code /openapi/openapi.2026-10.json post /apps/2026-10/offers/{id}/codes/{codeId}/disable A disabled code stops applying; its history and its spelling are kept (it cannot be added again). Disabling a disabled code changes nothing. An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `write_offers` scope (permission `offers:update`). # Enable a disabled code Source: https://developers.tassyir.io/api-reference/admin/offers/enable-a-disabled-code /openapi/openapi.2026-10.json post /apps/2026-10/offers/{id}/codes/{codeId}/enable Enabling an enabled code changes nothing. An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `write_offers` scope (permission `offers:update`). # Get an offer Source: https://developers.tassyir.io/api-reference/admin/offers/get-an-offer /openapi/openapi.2026-10.json get /apps/2026-10/offers/{id} Needs the `read_offers` scope (permission `offers:read`). # List an offer's codes Source: https://developers.tassyir.io/api-reference/admin/offers/list-an-offers-codes /openapi/openapi.2026-10.json get /apps/2026-10/offers/{id}/codes Newest first, disabled codes included. Only the owner reads them: An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `read_offers` scope (permission `offers:read`). # List an offer's redemptions Source: https://developers.tassyir.io/api-reference/admin/offers/list-an-offers-redemptions /openapi/openapi.2026-10.json get /apps/2026-10/offers/{id}/redemptions Each use of the offer on an order, newest first: the order id, the code and the amount it took off. Never the buyer: no name, no phone, no phone key. Only the owner reads them: An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `read_offers` scope (permission `offers:read`). # List offers Source: https://developers.tassyir.io/api-reference/admin/offers/list-offers /openapi/openapi.2026-10.json get /apps/2026-10/offers The merchant's per-store pricing offers (quantity offers, free delivery, product discounts), newest first, drafts included. A store-limited installation reads the offers of its stores. Codes are not exposed (their count is), and neither is any usage: never a buyer. Needs the `read_offers` scope (permission `offers:read`). # Pause an offer Source: https://developers.tassyir.io/api-reference/admin/offers/pause-an-offer /openapi/openapi.2026-10.json post /apps/2026-10/offers/{id}/pause Stops a live offer pricing orders; resume it to start again. Pausing a paused offer changes nothing. An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `write_offers` scope (permission `offers:update`). # Publish an offer Source: https://developers.tassyir.io/api-reference/admin/offers/publish-an-offer /openapi/openapi.2026-10.json post /apps/2026-10/offers/{id}/publish Puts a draft live (from its start date). An incomplete offer is 400 `OFFER_INVALID`; past the plan's `active_offers` cap it is 403 `PLAN_LIMIT_REACHED` (the cap counts an app's offers like the merchant's). Publishing a published offer changes nothing. An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `write_offers` scope (permission `offers:update`). # Resume a paused offer Source: https://developers.tassyir.io/api-reference/admin/offers/resume-a-paused-offer /openapi/openapi.2026-10.json post /apps/2026-10/offers/{id}/resume Starts a paused offer again; the plan's `active_offers` cap applies. An ended offer is not resumed: move its end date instead. Resuming an offer that is not paused changes nothing. An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `write_offers` scope (permission `offers:update`). # Update an offer Source: https://developers.tassyir.io/api-reference/admin/offers/update-an-offer /openapi/openapi.2026-10.json patch /apps/2026-10/offers/{id} Changes the fields sent (`targets`, when sent, replaces the whole target set; `kind` and `method` never change). A live offer must stay publishable. The change applies to the offer as it is now unless `expectedVersion` is sent (the dashboard's compare-and-set, 409 `OFFER_VERSION_CONFLICT` with `currentVersion`). An app changes, and reads the codes and redemptions of, only the offers its own installation created (`ownerInstallationId` is set from the token when it creates one). A merchant's offer, or another app's, is 403 `OFFER_NOT_OWNED`; an offer the token cannot see (another business, a store outside the installation's stores) is 404. Needs the `write_offers` scope (permission `offers:update`). # Cancel an order Source: https://developers.tassyir.io/api-reference/admin/orders/cancel-an-order /openapi/openapi.2026-10.json post /apps/2026-10/orders/{id}/cancel With one of `GET cancel-reasons` and a note when that reason asks for one. Recorded as cancelled by the app. Needs the `write_orders` scope (permission `orders:cancel`). # Comment on an order Source: https://developers.tassyir.io/api-reference/admin/orders/comment-on-an-order /openapi/openapi.2026-10.json post /apps/2026-10/orders/{id}/comments Needs the `write_orders` scope (permission `orders:add-comment`). # Confirm an order Source: https://developers.tassyir.io/api-reference/admin/orders/confirm-an-order /openapi/openapi.2026-10.json post /apps/2026-10/orders/{id}/confirm Needs the `write_orders` scope (permission `orders:confirm`). # Create an order Source: https://developers.tassyir.io/api-reference/admin/orders/create-an-order /openapi/openapi.2026-10.json post /apps/2026-10/orders The dashboard's manual create: allowance admit, one distribution pass, `source = app:`. Past the plan's allowance the order is created WAITING (phones hidden, confirm and dispatch locked). `codes` (one offer code at most) prices the order with that code when the business has order pricing on; a code that does not apply is listed in `rejectedCodes` beside `data` and the order is still created. On a dev store, orders are capped per day: 429 `DEV_STORE_ORDER_LIMIT_REACHED` with `retryAfterSeconds`. Needs the `write_orders` scope (permission `orders:create`). # Get an order Source: https://developers.tassyir.io/api-reference/admin/orders/get-an-order /openapi/openapi.2026-10.json get /apps/2026-10/orders/{id} Needs the `read_orders` scope (permission `orders:read`). # List an order's comments Source: https://developers.tassyir.io/api-reference/admin/orders/list-an-orders-comments /openapi/openapi.2026-10.json get /apps/2026-10/orders/{id}/comments Needs the `read_orders` scope (permission `orders:read`). # List orders Source: https://developers.tassyir.io/api-reference/admin/orders/list-orders /openapi/openapi.2026-10.json get /apps/2026-10/orders Every stage in one list, archived included, newest first. Never an undecided abandoned checkout nor an exchange-return row. Without `orders:read-all` (an online token of a member who sees only their own orders), only the orders assigned to that member. Needs the `read_orders` scope (permission `orders:read`). # Schedule a call-back Source: https://developers.tassyir.io/api-reference/admin/orders/schedule-a-call-back /openapi/openapi.2026-10.json post /apps/2026-10/orders/{id}/schedule Needs the `write_orders` scope (permission `orders:schedule`). # The merchant's cancel reasons Source: https://developers.tassyir.io/api-reference/admin/orders/the-merchants-cancel-reasons /openapi/openapi.2026-10.json get /apps/2026-10/cancel-reasons The active reasons, in the merchant's order, all at once (`nextCursor` is always null). Needs the `read_orders` scope (permission `orders:read`). # Update an order Source: https://developers.tassyir.io/api-reference/admin/orders/update-an-order /openapi/openapi.2026-10.json patch /apps/2026-10/orders/{id} The buyer, the delivery choice and the notes. Not the status (confirm, cancel and schedule have their own routes), not the items, prices, carrier or store. Needs the `write_orders` scope (permission `orders:update`). # Create a product Source: https://developers.tassyir.io/api-reference/admin/products/create-a-product /openapi/openapi.2026-10.json post /apps/2026-10/products Images must already be on the Tassyir CDN; a description is checked (no scripts, frames, forms, handlers, fetching CSS). Needs the `write_products` scope (permission `products:create`). # Get a product Source: https://developers.tassyir.io/api-reference/admin/products/get-a-product /openapi/openapi.2026-10.json get /apps/2026-10/products/{id} Needs the `read_products` scope (permission `products:read`). # List products Source: https://developers.tassyir.io/api-reference/admin/products/list-products /openapi/openapi.2026-10.json get /apps/2026-10/products Every product, active or not, newest first. A collection membership change does not move `updatedAt` (listen to `products/updated` for `collectionIds`). Needs the `read_products` scope (permission `products:read`). # Update a product Source: https://developers.tassyir.io/api-reference/admin/products/update-a-product /openapi/openapi.2026-10.json patch /apps/2026-10/products/{id} `options` and `variants` are replace-all, together: every `id` sent must be one of this product's variants; a variant left out is removed. Needs the `write_products` scope (permission `products:update`). # Update one variant's fields Source: https://developers.tassyir.io/api-reference/admin/products/update-one-variants-fields /openapi/openapi.2026-10.json patch /apps/2026-10/products/{id}/variants/{variantId} Only this variant is written; the others are sent back as they are. The default variant follows its product and is changed through it. Needs the `write_products` scope (permission `products:update`). # Get a store Source: https://developers.tassyir.io/api-reference/admin/stores/get-a-store /openapi/openapi.2026-10.json get /apps/2026-10/stores/{id} Needs the `read_stores` scope (permission `stores:read`). # List stores Source: https://developers.tassyir.io/api-reference/admin/stores/list-stores /openapi/openapi.2026-10.json get /apps/2026-10/stores Needs the `read_stores` scope (permission `stores:read`). # Abandoned checkoutscreated Source: https://developers.tassyir.io/api-reference/admin/webhooks/abandoned_checkoutscreated /openapi/openapi.2026-10.json webhook abandoned_checkouts/created An abandoned checkout was captured. Sent to installations granted `read_orders` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Appinstalled Source: https://developers.tassyir.io/api-reference/admin/webhooks/appinstalled /openapi/openapi.2026-10.json webhook app/installed The app was installed on a business. About the installation itself: sent to that installation only. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Appscopes updated Source: https://developers.tassyir.io/api-reference/admin/webhooks/appscopes_updated /openapi/openapi.2026-10.json webhook app/scopes_updated The merchant granted a new set of scopes. About the installation itself: sent to that installation only. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Appuninstalled Source: https://developers.tassyir.io/api-reference/admin/webhooks/appuninstalled /openapi/openapi.2026-10.json webhook app/uninstalled The app was uninstalled; its tokens are already revoked. About the installation itself: sent to that installation only. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Bulkfinished Source: https://developers.tassyir.io/api-reference/admin/webhooks/bulkfinished /openapi/openapi.2026-10.json webhook bulk/finished A bulk export this installation asked for finished: `completed` (download it from `url`) or `failed` (`errorCode`). Sent only to that installation. About the installation itself: sent to that installation only. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Businessredact Source: https://developers.tassyir.io/api-reference/admin/webhooks/businessredact /openapi/openapi.2026-10.json webhook business/redact Delete what you hold about this business: the app was uninstalled 48 h ago and Tassyir has deleted its app data. A compliance topic: sent to that uninstalled installation only, when its version subscribes; never to another app. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Customersdata request Source: https://developers.tassyir.io/api-reference/admin/webhooks/customersdata_request /openapi/openapi.2026-10.json webhook customers/data_request A buyer asked for the data held about them. A compliance topic: every installation that subscribes receives it, whatever its scopes. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Customersredact Source: https://developers.tassyir.io/api-reference/admin/webhooks/customersredact /openapi/openapi.2026-10.json webhook customers/redact Delete what you hold about this buyer. A compliance topic: every installation that subscribes receives it, whatever its scopes. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Customersupdated Source: https://developers.tassyir.io/api-reference/admin/webhooks/customersupdated /openapi/openapi.2026-10.json webhook customers/updated A customer's record changed. Sent to installations granted `read_customers` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Inventory levelsupdated Source: https://developers.tassyir.io/api-reference/admin/webhooks/inventory_levelsupdated /openapi/openapi.2026-10.json webhook inventory_levels/updated A variant's on-hand, in-transit or damaged stock changed. Sent to installations granted `read_inventory` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Offerscreated Source: https://developers.tassyir.io/api-reference/admin/webhooks/offerscreated /openapi/openapi.2026-10.json webhook offers/created An offer was created (as a draft). Sent to installations granted `read_offers` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Offersdeleted Source: https://developers.tassyir.io/api-reference/admin/webhooks/offersdeleted /openapi/openapi.2026-10.json webhook offers/deleted An offer that was never redeemed was deleted; only its id remains. Sent to installations granted `read_offers` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Offersupdated Source: https://developers.tassyir.io/api-reference/admin/webhooks/offersupdated /openapi/openapi.2026-10.json webhook offers/updated An offer changed (publishing, pausing, archiving, its dates, rules and codes included); `changed` names the fields. Sent to installations granted `read_offers` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Orderscancelled Source: https://developers.tassyir.io/api-reference/admin/webhooks/orderscancelled /openapi/openapi.2026-10.json webhook orders/cancelled An order was cancelled. Sent to installations granted `read_orders` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Ordersconfirmed Source: https://developers.tassyir.io/api-reference/admin/webhooks/ordersconfirmed /openapi/openapi.2026-10.json webhook orders/confirmed An order was confirmed. Sent to installations granted `read_orders` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Orderscreated Source: https://developers.tassyir.io/api-reference/admin/webhooks/orderscreated /openapi/openapi.2026-10.json webhook orders/created A real order was created (never an abandoned checkout), after the order allowance decided it. Sent to installations granted `read_orders` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Ordersdelivered Source: https://developers.tassyir.io/api-reference/admin/webhooks/ordersdelivered /openapi/openapi.2026-10.json webhook orders/delivered An order was delivered. Sent to installations granted `read_orders` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Ordersdispatched Source: https://developers.tassyir.io/api-reference/admin/webhooks/ordersdispatched /openapi/openapi.2026-10.json webhook orders/dispatched An order was handed to a carrier. Sent to installations granted `read_orders` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Ordersreturned Source: https://developers.tassyir.io/api-reference/admin/webhooks/ordersreturned /openapi/openapi.2026-10.json webhook orders/returned An order came back (its status does not change: `returned` is true). Sent to installations granted `read_orders` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Ordersupdated Source: https://developers.tassyir.io/api-reference/admin/webhooks/ordersupdated /openapi/openapi.2026-10.json webhook orders/updated Any other change to a field an app can read; `changed` names the fields. Sent to installations granted `read_orders` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Productscreated Source: https://developers.tassyir.io/api-reference/admin/webhooks/productscreated /openapi/openapi.2026-10.json webhook products/created A product was created. Sent to installations granted `read_products` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Productsdeleted Source: https://developers.tassyir.io/api-reference/admin/webhooks/productsdeleted /openapi/openapi.2026-10.json webhook products/deleted A product was deleted; only its ids remain. Sent to installations granted `read_products` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Productsupdated Source: https://developers.tassyir.io/api-reference/admin/webhooks/productsupdated /openapi/openapi.2026-10.json webhook products/updated A product changed (deactivation included: `active` turns false). Sent to installations granted `read_products` whose version subscribes to the topic. At least once, no ordering guarantee. `data` is read when the body is built (seconds after the event), masked for the installation. # Accept the developer terms Source: https://developers.tassyir.io/api-reference/developer/account/accept-the-developer-terms /openapi/openapi.developer.json post /developer/terms Records that you accepted the "Developer terms for free apps" version `2026-10` (the only one accepted today; read `terms.currentVersion` on your account). Needed before a listing submission; a new version asks again before your next one. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Change your developer account Source: https://developers.tassyir.io/api-reference/developer/account/change-your-developer-account /openapi/openapi.developer.json patch /developer/account `websiteUrl: null` clears it; `null` is refused for the other two. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Get your developer account Source: https://developers.tassyir.io/api-reference/developer/account/get-your-developer-account /openapi/openapi.developer.json get /developer/account Null when developer mode is off: not an error, the console asks this once to decide what to draw. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Turn developer mode on Source: https://developers.tassyir.io/api-reference/developer/account/turn-developer-mode-on /openapi/openapi.developer.json post /developer/account Every field is optional: `displayName` defaults to your name (else the part of your email before `@`), `supportEmail` to your email. `displayName` follows the manifest's `name` rule (1-50 characters on one line). Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Change an app Source: https://developers.tassyir.io/api-reference/developer/apps/change-an-app /openapi/openapi.developer.json patch /developer/apps/{appId} Its names and `customBusinessId` (`null` clears it). The handle, client id, hosting mode and distribution never change. A LISTED app's name changes through its listing (`409 APP_NAME_LOCKED`). Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Create an app Source: https://developers.tassyir.io/api-reference/developer/apps/create-an-app /openapi/openapi.developer.json post /developer/apps It has no version and no secret yet. `handle` is permanent (the manifest's `handle` rule, unique on Tassyir); `clientId` is minted here and never changes. `hostingMode: tassyir` is refused while Tassyir Hosting is switched off or not open to your developer account; the mode follows the app's current version. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Get one of your apps Source: https://developers.tassyir.io/api-reference/developer/apps/get-one-of-your-apps /openapi/openapi.developer.json get /developer/apps/{appId} Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # List your apps Source: https://developers.tassyir.io/api-reference/developer/apps/list-your-apps /openapi/openapi.developer.json get /developer/apps Newest first, at most 50 (an account holds at most 50 apps). Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Approve a CLI sign-in Source: https://developers.tassyir.io/api-reference/developer/cli-sign-in/approve-a-cli-sign-in /openapi/openapi.developer.json post /developer/cli/device/approve The approval page's route (dashboard JWT ONLY: a developer token can never approve a sign-in). The code travels in the body; dashes, spaces and case are ignored. An unknown, expired or someone else's code is one answer, 404 `CLI_CODE_NOT_FOUND`. 10 approvals per user an hour; 10 wrong codes per user and 30 per IP an hour. Only on the developer's click, never automatically. Idempotent. The CLI's next poll receives the token, and the developer is told (a bell notification and a push). Takes the dashboard session ONLY: a developer token is refused here, so a stolen CLI token can never approve a new sign-in. # Deny a CLI sign-in Source: https://developers.tassyir.io/api-reference/developer/cli-sign-in/deny-a-cli-sign-in /openapi/openapi.developer.json post /developer/cli/device/deny The approval page's route (dashboard JWT ONLY: a developer token can never approve a sign-in). The code travels in the body; dashes, spaces and case are ignored. An unknown, expired or someone else's code is one answer, 404 `CLI_CODE_NOT_FOUND`. 10 approvals per user an hour; 10 wrong codes per user and 30 per IP an hour. Works until the token is issued; a suspended account may still deny. Takes the dashboard session ONLY: a developer token is refused here, so a stolen CLI token can never approve a new sign-in. # Look a sign-in up by its user code Source: https://developers.tassyir.io/api-reference/developer/cli-sign-in/look-a-sign-in-up-by-its-user-code /openapi/openapi.developer.json post /developer/cli/device/lookup The approval page's route (dashboard JWT ONLY: a developer token can never approve a sign-in). The code travels in the body; dashes, spaces and case are ignored. An unknown, expired or someone else's code is one answer, 404 `CLI_CODE_NOT_FOUND`. 10 approvals per user an hour; 10 wrong codes per user and 30 per IP an hour. Show what is asking (client, device name, when, the country it started from) before the developer decides. Takes the dashboard session ONLY: a developer token is refused here, so a stolen CLI token can never approve a new sign-in. # Poll for the developer token (RFC 8628 §3.4) Source: https://developers.tassyir.io/api-reference/developer/cli-sign-in/poll-for-the-developer-token-rfc-8628-§34 /openapi/openapi.developer.json post /developer/cli/token Every `interval` seconds: `authorization_pending` until the developer decides; `slow_down` (HTTP 400) when a poll comes too soon, after which the interval is 5 seconds longer (at most 60); stop on `access_denied`, `expired_token` or `invalid_grant`. A device code yields one token, once. 30 unknown device codes per IP a minute (429 `slow_down`). Public: the CLI holds nothing yet. # Start a CLI sign-in (RFC 8628 §3.1) Source: https://developers.tassyir.io/api-reference/developer/cli-sign-in/start-a-cli-sign-in-rfc-8628-§31 /openapi/openapi.developer.json post /developer/cli/device-code Print `user_code`, open `verification_uri_complete` (it only pre-fills the code: the developer approves on the page, signed in to the dashboard), then poll `POST /developer/cli/token` every `interval` seconds. 10 sign-ins per IP every 10 minutes. Errors are OAuth's `{ error, error_description }`. Public: the CLI holds nothing yet. # List your live developer tokens Source: https://developers.tassyir.io/api-reference/developer/cli-tokens/list-your-live-developer-tokens /openapi/openapi.developer.json get /developer/cli/tokens Newest first, at most 100; `current` marks the one this request was made with. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Revoke one of your developer tokens Source: https://developers.tassyir.io/api-reference/developer/cli-tokens/revoke-one-of-your-developer-tokens /openapi/openapi.developer.json delete /developer/cli/tokens/{tokenId} Revoking a revoked token answers the same. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Revoke the token this request is made with (`tassyir logout`) Source: https://developers.tassyir.io/api-reference/developer/cli-tokens/revoke-the-token-this-request-is-made-with-`tassyir-logout` /openapi/openapi.developer.json delete /developer/cli/tokens/current Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Sign out of every CLI Source: https://developers.tassyir.io/api-reference/developer/cli-tokens/sign-out-of-every-cli /openapi/openapi.developer.json delete /developer/cli/tokens Revokes every token of yours, at once (tokens are never cached). Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Create a dev store Source: https://developers.tassyir.io/api-reference/developer/dev-stores/create-a-dev-store /openapi/openapi.developer.json post /developer/dev-stores A business you own, with one store on a `dev-xxxxxxxx` subdomain and, unless `seed: false`, synthetic data (30 products, 64 buyers on fake phones, 200 orders in every state over 58 wilayas). It reaches no outside party and costs nothing. At most 3 per developer, 3 creates a minute. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # List your dev stores Source: https://developers.tassyir.io/api-reference/developer/dev-stores/list-your-dev-stores /openapi/openapi.developer.json get /developer/dev-stores Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # List an app's installations, with their webhooks' health Source: https://developers.tassyir.io/api-reference/developer/installations/list-an-apps-installations-with-their-webhooks-health /openapi/openapi.developer.json get /developer/apps/{appId}/installations Newest first. `limit` 1-100 (default 50). Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Delete a listing image Source: https://developers.tassyir.io/api-reference/developer/listing/delete-a-listing-image /openapi/openapi.developer.json delete /developer/apps/{appId}/listing/media/{mediaId} Refused while your draft names it (remove it from the draft first) or the live listing shows it. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Get your app's listing Source: https://developers.tassyir.io/api-reference/developer/listing/get-your-apps-listing /openapi/openapi.developer.json get /developer/apps/{appId}/listing The draft you edit (an app without one reads its names only), what it still needs before you can submit it (`issues`), the live listing the marketplace shows (null until Tassyir approves one) and every listing image. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Replace your app's draft listing Source: https://developers.tassyir.io/api-reference/developer/listing/replace-your-apps-draft-listing /openapi/openapi.developer.json put /developer/apps/{appId}/listing The whole draft: a field you leave out (or send `null`) is cleared. Every rule is checked at once (`LISTING_INVALID` lists each problem with its `path`); an incomplete draft is saved and its gaps come back as `issues`. Plain text only: no HTML, no Markdown links, no invisible characters. The draft never reaches the marketplace by itself: Tassyir reviews it first. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Upload a listing image Source: https://developers.tassyir.io/api-reference/developer/listing/upload-a-listing-image /openapi/openapi.developer.json post /developer/apps/{appId}/listing/media One image a call, as `multipart/form-data`: the file in `file`, its `kind`. The BYTES decide what it is: PNG, JPEG or WebP (never SVG or GIF), at most 2 MB. An icon is square, 512 to 1200 px; a desktop screenshot 16:9 from 1600x900 to 3200x1800; a mobile one 9:16 from 900x1600 to 1800x3200. An app holds at most 20 images (drafts included), and a developer uploads at most 30 every 10 minutes. An image no draft names is deleted 7 days after it was uploaded. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Delete your reply to a rating Source: https://developers.tassyir.io/api-reference/developer/ratings/delete-your-reply-to-a-rating /openapi/openapi.developer.json delete /developer/apps/{appId}/ratings/{ratingId}/reply Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # List your app's ratings Source: https://developers.tassyir.io/api-reference/developer/ratings/list-your-apps-ratings /openapi/openapi.developer.json get /developer/apps/{appId}/ratings The published ratings, newest first, with the average and count the marketplace shows. `limit` 1-50 (default 20). A rating Tassyir hid is not listed. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Reply to a rating Source: https://developers.tassyir.io/api-reference/developer/ratings/reply-to-a-rating /openapi/openapi.developer.json put /developer/apps/{appId}/ratings/{ratingId}/reply One reply per rating, shown under it; sending again replaces it. Plain text, 1-1,000 characters, without links, email addresses, phone numbers or insults. The merchant is told of your first reply. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Report a rating to Tassyir Source: https://developers.tassyir.io/api-reference/developer/ratings/report-a-rating-to-tassyir /openapi/openapi.developer.json post /developer/apps/{appId}/ratings/{ratingId}/report A rating that is off topic, shares personal data, comes from a competitor, is abusive or is spam. Tassyir decides; reporting again while your report is open changes nothing. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Add a note for the reviewer Source: https://developers.tassyir.io/api-reference/developer/review/add-a-note-for-the-reviewer /openapi/openapi.developer.json post /developer/apps/{appId}/submissions/{reviewId}/notes Plain text: test steps, a demo account on your service, why the version needs a scope. While the case is open; at most 20 notes. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Get a review case Source: https://developers.tassyir.io/api-reference/developer/review/get-a-review-case /openapi/openapi.developer.json get /developer/apps/{appId}/submissions/{reviewId} Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # List your app's review cases Source: https://developers.tassyir.io/api-reference/developer/review/list-your-apps-review-cases /openapi/openapi.developer.json get /developer/apps/{appId}/submissions Newest first: your submissions and the `version` cases a new version opened. `limit` 1-100 (default 20). Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Put your app back in the marketplace Source: https://developers.tassyir.io/api-reference/developer/review/put-your-app-back-in-the-marketplace /openapi/openapi.developer.json post /developer/apps/{appId}/relist Only after you delisted it: an app Tassyir delisted is relisted by Tassyir (`APP_NOT_LISTABLE` `delisted_by_tassyir`). Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Read a dry run of the review checks Source: https://developers.tassyir.io/api-reference/developer/review/read-a-dry-run-of-the-review-checks /openapi/openapi.developer.json get /developer/apps/{appId}/review-checks/{runId} Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Run the review checks as a dry run Source: https://developers.tassyir.io/api-reference/developer/review/run-the-review-checks-as-a-dry-run /openapi/openapi.developer.json post /developer/apps/{appId}/review-checks What a submission would find now, on your draft listing and latest version, without submitting: every check of the checklist (it fetches your privacy policy and admin page and sends test deliveries, signed and forged, to your webhook endpoints). Queued: read the result with the run id (kept 24 hours). At most 5 per app per hour. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Submit the app for the marketplace Source: https://developers.tassyir.io/api-reference/developer/review/submit-the-app-for-the-marketplace /openapi/openapi.developer.json post /developer/apps/{appId}/submissions Snapshots your DRAFT listing and names your LATEST version, for `public` (browsed and searched) or `unlisted` (installs from its link) distribution: a `listing` review case, `checking` first (the automated checklist), then `submitted` or `checks_failed`. A new submission replaces an open case. Needs the current developer terms accepted. Tassyir aims at 5 working days for a first review, 2 for a resubmission. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Take your app out of the marketplace Source: https://developers.tassyir.io/api-reference/developer/review/take-your-app-out-of-the-marketplace /openapi/openapi.developer.json post /developer/apps/{appId}/delist Out of the catalogue and search, no new installs; existing installations keep working. Undo with relist. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Withdraw a review case Source: https://developers.tassyir.io/api-reference/developer/review/withdraw-a-review-case /openapi/openapi.developer.json post /developer/apps/{appId}/submissions/{reviewId}/withdraw An open case only. A version that waited for it is retired (never released); upload a new one when ready. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # List an app's client secrets Source: https://developers.tassyir.io/api-reference/developer/secrets/list-an-apps-client-secrets /openapi/openapi.developer.json get /developer/apps/{appId}/credentials Newest first, at most 50. Never a secret, never its hash. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Mint a client secret (the first, or a rotation) Source: https://developers.tassyir.io/api-reference/developer/secrets/mint-a-client-secret-the-first-or-a-rotation /openapi/openapi.developer.json post /developer/apps/{appId}/credentials The secret is in this answer ONCE. Minting while other secrets are live is the rotation: each live secret with no end date gets `expiresAt = now + 24 h`. At most 3 live secrets per app; 10 minted per developer per hour. No body. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Revoke a client secret now Source: https://developers.tassyir.io/api-reference/developer/secrets/revoke-a-client-secret-now /openapi/openapi.developer.json delete /developer/apps/{appId}/credentials/{credentialId} The token endpoint refuses it from this moment; access tokens already issued live out their hour. Idempotent. Allowed on a suspended account and a read-only app: a leaked secret must always be killable. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Get one version, with its manifest Source: https://developers.tassyir.io/api-reference/developer/versions/get-one-version-with-its-manifest /openapi/openapi.developer.json get /developer/apps/{appId}/versions/{version} Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # List an app's versions Source: https://developers.tassyir.io/api-reference/developer/versions/list-an-apps-versions /openapi/openapi.developer.json get /developer/apps/{appId}/versions Newest first, without their manifests. `limit` 1-100 (default 20); `cursor` is the previous `nextCursor` (versions below that number). Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Upload a version of the app's manifest Source: https://developers.tassyir.io/api-reference/developer/versions/upload-a-version-of-the-apps-manifest /openapi/openapi.developer.json post /developer/apps/{appId}/versions The body is at most 328 KB (8 MB + 128 KB while Tassyir Hosting is on) and nests at most 16 levels (its own parser); a larger body is 413 `PAYLOAD_TOO_LARGE` with `limitBytes`, `hostingAvailable` and a `fix` saying why (while hosting is off, only a version carrying a bundle is that large). The validator checks the manifest (every error at once); `handle` must be the app's; a manifest identical to the latest version's (and, hosted, the same bundle) is refused. An external version is numbered latest + 1, approved and current at once, except for a LISTED app: there it must subscribe the three compliance topics and name a supported API version (400 `REVIEW_CHECKS_FAILED`, nothing stored), and a version that adds scopes, data hosts or storefront code waits `in_review` for a person (a `version` review case opens by itself). A hosted version (`hosting.mode = "tassyir"`, while Tassyir Hosting is on and open to your developer account) carries its `bundle`, stored before the version is created `scanning`; it is scanned, uploaded and released by the hosting pipeline. Storefront blocks (`[[storefront.blocks]]`, scope `write_storefront`): each block's template and stylesheet are read from the version's files (an external version's `assets`, a hosted version's `bundle.files`) and checked by `@tassyir/storefront-blocks`' `validateBlock`, the same function the CLI runs; the first block that fails is 400 `BLOCK_TEMPLATE_INVALID` with the package's errors (each with `line`, `column` and `fix`), a missing, unused or oversized file 400 `BLOCK_ASSETS_INVALID`, and nothing is stored. 30 uploads per developer per hour, 10 hosted a day. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # One delivery of the log Source: https://developers.tassyir.io/api-reference/developer/webhooks/one-delivery-of-the-log /openapi/openapi.developer.json get /developer/apps/{appId}/webhooks/deliveries/{deliveryId} Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Re-enable an installation's webhooks Source: https://developers.tassyir.io/api-reference/developer/webhooks/re-enable-an-installations-webhooks /openapi/openapi.developer.json post /developer/apps/{appId}/installations/{installationId}/webhooks/enable After a 410 or 3 days of continuous failure disabled them: clears the disable and the failure clock. Deliveries recorded `skipped` meanwhile stay skipped (replay the ones you need). Idempotent. No body. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Send a delivery again, now Source: https://developers.tassyir.io/api-reference/developer/webhooks/send-a-delivery-again-now /openapi/openapi.developer.json post /developer/apps/{appId}/webhooks/deliveries/{deliveryId}/replay One more attempt through the real sender, inline, with the same `webhook-id` (a receiver that deduplicates on it ignores a replay of something it processed). The attempt is final: no retry schedule after a replay. Refused while the delivery is still pending. 60 replays per app and 120 per developer every 10 minutes. No body. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Send a test delivery to an installation Source: https://developers.tassyir.io/api-reference/developer/webhooks/send-a-test-delivery-to-an-installation /openapi/openapi.developer.json post /developer/apps/{appId}/installations/{installationId}/webhooks/test A synthetic, signed delivery of a sample body (`tassyir-test: true`, `webhook-id: dlv_test_…`, envelope id `evt_0`); nothing is recorded and a 410 disables nothing. Needs an active installation whose version subscribes to the topic and was granted its scope. 20 tests per app and 40 per developer every 10 minutes. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # The app's delivery log Source: https://developers.tassyir.io/api-reference/developer/webhooks/the-apps-delivery-log /openapi/openapi.developer.json get /developer/apps/{appId}/webhooks/deliveries Newest first, the last 30 days, metadata only (never a body or a URL). `limit` 1-100 (default 50); `since` is inclusive, `until` exclusive. Takes the dashboard session or a developer token (`tsy_dt_…`); either way the caller is the signed-in user, and only their own account, apps and tokens are reachable. # Claude Code plugin Source: https://developers.tassyir.io/claude-code Give Claude Code the Tassyir app contract and the developer MCP server with one plugin. The `tassyir` plugin gives Claude Code what it needs to build a Tassyir app: * the **developer MCP server**, [`@tassyir/dev-mcp`](/packages/dev-mcp): it searches and reads these guides, looks up any Admin API operation and webhook body, validates `tassyir.app.toml` with the API's own validator, starts a new app from the starter, and sends test webhooks and creates dev stores with your `tassyir login`; * the **`tassyir-app` skill**: the app contract (manifest, scopes, tokens, webhooks, Admin API, App Bridge, dev stores), the same file [`create-tassyir-app`](/packages/create-tassyir-app) writes into every new project. ## Install In Claude Code, add Tassyir's plugin marketplace, then install the plugin: ```text theme={null} /plugin marketplace add tassyir/apps-sdk /plugin install tassyir@tassyir ``` From a terminal, the same is: ```bash theme={null} claude plugin marketplace add tassyir/apps-sdk claude plugin install tassyir@tassyir ``` The marketplace is the [`tassyir/apps-sdk`](https://github.com/tassyir/apps-sdk) repository. The plugin runs the `@tassyir/dev-mcp` release it names, with `npx`, so you need Node 22.18 or later. ## Update For a newer release later: ```bash theme={null} claude plugin marketplace update tassyir claude plugin update tassyir@tassyir ``` ## Without the plugin The server alone works in any MCP client. In Claude Code: ```bash theme={null} claude mcp add tassyir -- npx -y @tassyir/dev-mcp ``` The tools that act on your Tassyir account use the sign-in of the [`tassyir` CLI](/packages/cli). Sign in once, in a terminal: ```bash theme={null} npx tassyir login ``` Every tool, its input and its answers: [`@tassyir/dev-mcp`](/packages/dev-mcp). # API_BUSY Source: https://developers.tassyir.io/errors/api-busy HTTP 503, from every operation of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/api-busy`; its `title`: API busy. ## What to do HTTP 503. The API was too busy to take your call and did **nothing**. Wait `Retry-After` (1 second) and send the same call again, a write included. The answer's `fix`: Nothing was done: wait the number of seconds in Retry-After, then retry with exponential backoff. ## Answered by Every operation of the Admin API, with HTTP 503. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # APP_DATA_KEY_NOT_DECLARED Source: https://developers.tassyir.io/errors/app-data-key-not-declared HTTP 400, from PUT /data/{ownerType}/{ownerId}/{key}. Its problem `type`: `https://developers.tassyir.io/errors/app-data-key-not-declared`; its `title`: App data key not declared. ## What to do HTTP 400. Your installation's version does not declare that key for that owner type. Declare it under `[[data.definitions]]`, upload, and have the merchant accept the update. The answer's `fix`: Declare the key for this owner under \[\[data.definitions]] in the app's manifest; it can be written once the merchant accepts that version. ## Answered by HTTP 400, from: * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # APP_DATA_LIMIT_REACHED Source: https://developers.tassyir.io/errors/app-data-limit-reached HTTP 409, from PUT /data/{ownerType}/{ownerId}/{key}. Its problem `type`: `https://developers.tassyir.io/errors/app-data-limit-reached`; its `title`: App data limit reached. ## What to do Delete values the app no longer needs with DELETE /data/\{ownerType}/\{ownerId}/\{key}, then retry; rewriting a key that exists is never refused. ## Answered by HTTP 409, from: * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # APP_DATA_VALUE_INVALID Source: https://developers.tassyir.io/errors/app-data-value-invalid HTTP 400, from PUT /data/{ownerType}/{ownerId}/{key}. Its problem `type`: `https://developers.tassyir.io/errors/app-data-value-invalid`; its `title`: App data value invalid. ## What to do Send a value of the declaredType the manifest gives this key, without the part named in reason. ## Answered by HTTP 400, from: * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # APP_DATA_VALUE_TOO_LARGE Source: https://developers.tassyir.io/errors/app-data-value-too-large HTTP 400, from PUT /data/{ownerType}/{ownerId}/{key}. Its problem `type`: `https://developers.tassyir.io/errors/app-data-value-too-large`; its `title`: App data value too large. ## What to do Keep each value within limitBytes of compact JSON: split it across several keys, or store a reference instead. ## Answered by HTTP 400, from: * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # APP_TOKEN_INVALID Source: https://developers.tassyir.io/errors/app-token-invalid HTTP 401, from every operation of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/app-token-invalid`; its `title`: Access token invalid. ## What to do HTTP 401. The token is unknown, expired or revoked, or its installation or app is no longer active. Refresh it once (the SDK does). If the refresh is refused, the installation was uninstalled, suspended or had its scopes changed: an embedded app gets a new token on the next page view, a headless app needs a new install code. The answer's `fix`: Get a new access token from POST /apps/oauth/token, then retry the request. ## Answered by Every operation of the Admin API, with HTTP 401. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # APP_TOKEN_REQUIRED Source: https://developers.tassyir.io/errors/app-token-required HTTP 401, from every operation of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/app-token-required`; its `title`: Access token required. ## What to do HTTP 401. No `Authorization: Bearer` header. Send the installation's access token. The answer's `fix`: Send the access token in the Authorization header as Bearer tsy\_at\_…. ## Answered by Every operation of the Admin API, with HTTP 401. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # BAD_REQUEST Source: https://developers.tassyir.io/errors/bad-request HTTP 400, from 23 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/bad-request`; its `title`: Bad request. ## What to do Correct the request as the detail says, then retry. ## Answered by HTTP 400, from: * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/comments`](/api-reference/admin/orders/comment-on-an-order) * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) Also the code of a refusal with HTTP 400 that carries no more specific code. A hosted app's `ctx.props.TASSYIR` binding also answers it itself, before the call is sent ([Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api)). The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # BRAND_NOT_FOUND Source: https://developers.tassyir.io/errors/brand-not-found HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/brand-not-found`; its `title`: Bad request. ## What to do Send the id of a brand of the product's store, or null for none; the ids in brandIds are not. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # BULK_EXPORT_IN_PROGRESS Source: https://developers.tassyir.io/errors/bulk-export-in-progress HTTP 409, from POST /bulk/exports. Its problem `type`: `https://developers.tassyir.io/errors/bulk-export-in-progress`; its `title`: Conflict. ## What to do Wait for the export named in bulkExportId to finish (the bulk/finished webhook, or GET /bulk/exports/\{id}), then ask again. ## Answered by HTTP 409, from: * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # BULK_EXPORT_RATE_LIMITED Source: https://developers.tassyir.io/errors/bulk-export-rate-limited HTTP 429, from POST /bulk/exports. Its problem `type`: `https://developers.tassyir.io/errors/bulk-export-rate-limited`; its `title`: Too many requests. ## What to do Wait the number of seconds in Retry-After; keep a finished export and read it again instead of exporting the same list twice. ## Answered by HTTP 429, from: * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # BULK_EXPORTS_NOT_CONFIGURED Source: https://developers.tassyir.io/errors/bulk-exports-not-configured HTTP 503, from POST /bulk/exports. Its problem `type`: `https://developers.tassyir.io/errors/bulk-exports-not-configured`; its `title`: Service unavailable. ## What to do Page through the list route instead, or retry later. ## Answered by HTTP 503, from: * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # BUSINESS_RATE_LIMITED Source: https://developers.tassyir.io/errors/business-rate-limited HTTP 429, from every operation of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/business-rate-limited`; its `title`: Business's rate limit exceeded. ## What to do HTTP 429. All the apps of this business together are past its ceiling. Wait `Retry-After` seconds; other apps are using the room, so slow down. The answer's `fix`: Wait the number of seconds in Retry-After, then retry; every app of this business shares this budget, so spread requests over time. ## Answered by Every operation of the Admin API, with HTTP 429. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # CANCEL_NOTE_REQUIRED Source: https://developers.tassyir.io/errors/cancel-note-required HTTP 400, from POST /orders/{id}/cancel. Its problem `type`: `https://developers.tassyir.io/errors/cancel-note-required`; its `title`: Bad request. ## What to do Send a note with this cancel reason: the merchant set it to require one. ## Answered by HTTP 400, from: * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # CANCEL_REASON_INVALID Source: https://developers.tassyir.io/errors/cancel-reason-invalid HTTP 400, from POST /orders/{id}/cancel. Its problem `type`: `https://developers.tassyir.io/errors/cancel-reason-invalid`; its `title`: Bad request. ## What to do Read GET /cancel-reasons again and send the id of one of its reasons, or no reason. ## Answered by HTTP 400, from: * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # COLLECTION_CONDITIONAL_LIMIT Source: https://developers.tassyir.io/errors/collection-conditional-limit HTTP 409, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/collection-conditional-limit`; its `title`: Conflict. ## What to do Make this collection without conditions, or remove the conditions of another first: the store has its maximum of collections with conditions (limit). ## Answered by HTTP 409, from: * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # COLLECTION_DEPTH_LIMIT Source: https://developers.tassyir.io/errors/collection-depth-limit HTTP 409, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/collection-depth-limit`; its `title`: Conflict. ## What to do Choose a parent higher in the tree: collections nest at most limit levels deep, with their subcollections. ## Answered by HTTP 409, from: * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # COLLECTION_NOT_FOUND Source: https://developers.tassyir.io/errors/collection-not-found HTTP 400, from POST /products. Its problem `type`: `https://developers.tassyir.io/errors/collection-not-found`; its `title`: Bad request. ## What to do Name only collections of the product's store that still exist; the ids in categoryIds are not. ## Answered by HTTP 400, from: * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # COLLECTION_PARENT_CYCLE Source: https://developers.tassyir.io/errors/collection-parent-cycle HTTP 409, from PATCH /collections/{id}. Its problem `type`: `https://developers.tassyir.io/errors/collection-parent-cycle`; its `title`: Conflict. ## What to do Choose a parent that is neither this collection nor one of its own subcollections. ## Answered by HTTP 409, from: * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # COLLECTION_PARENT_NOT_FOUND Source: https://developers.tassyir.io/errors/collection-parent-not-found HTTP 404, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/collection-parent-not-found`; its `title`: Not found. ## What to do Send as parentId a collection of the same store that still exists, or null for a top-level collection. ## Answered by HTTP 404, from: * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # COLLECTION_PRODUCT_WRONG_STORE Source: https://developers.tassyir.io/errors/collection-product-wrong-store HTTP 400, from PATCH /collections/{id}/products. Its problem `type`: `https://developers.tassyir.io/errors/collection-product-wrong-store`; its `title`: Bad request. ## What to do Add only products of the collection's store; the ids in productIds are not. ## Answered by HTTP 400, from: * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # COLLECTION_RULE_INVALID Source: https://developers.tassyir.io/errors/collection-rule-invalid HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/collection-rule-invalid`; its `title`: Bad request. ## What to do Correct the condition at index as reason says; a brand or tag it names must be the store's own. ## Answered by HTTP 400, from: * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # CONFLICT Source: https://developers.tassyir.io/errors/conflict A refusal with HTTP 409 that carries no more specific code. Its problem `type`: `https://developers.tassyir.io/errors/conflict`; its `title`: Conflict. ## What to do Read the resource again and act on its current state; do not retry the same request unchanged. ## Answered by The code of a refusal with HTTP 409 that carries no more specific code. Any operation may answer it. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # DELIVERY_SPEED_UNAVAILABLE Source: https://developers.tassyir.io/errors/delivery-speed-unavailable HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/delivery-speed-unavailable`; its `title`: Bad request. ## What to do Use the standard speed: economy is not priced for this commune and delivery method. ## Answered by HTTP 400, from: * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`POST /orders`](/api-reference/admin/orders/create-an-order) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # DELIVERY_SPEED_UNSUPPORTED Source: https://developers.tassyir.io/errors/delivery-speed-unsupported HTTP 400, from PATCH /orders/{id}. Its problem `type`: `https://developers.tassyir.io/errors/delivery-speed-unsupported`; its `title`: Bad request. ## What to do Use the standard speed: the order's carrier has no economy service. ## Answered by HTTP 400, from: * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # DEV_STORE_ORDER_LIMIT_REACHED Source: https://developers.tassyir.io/errors/dev-store-order-limit-reached HTTP 429, from POST /orders. Its problem `type`: `https://developers.tassyir.io/errors/dev-store-order-limit-reached`; its `title`: Too many requests. ## What to do Wait the number of seconds in retryAfterSeconds (until the next day in Algiers): a dev store takes at most limit new orders a day. ## Answered by HTTP 429, from: * [`POST /orders`](/api-reference/admin/orders/create-an-order) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # FORBIDDEN Source: https://developers.tassyir.io/errors/forbidden HTTP 403, from 4 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/forbidden`; its `title`: Forbidden. ## What to do Do not retry the same request: the detail says what this installation may not do. ## Answered by HTTP 403, from: * [`GET /inventory-levels`](/api-reference/admin/inventory/list-stock-levels) * [`GET /inventory-levels/{id}`](/api-reference/admin/inventory/get-a-stock-level) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /orders`](/api-reference/admin/orders/create-an-order) Also the code of a refusal with HTTP 403 that carries no more specific code. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # HOSTED_CALL_REFUSED Source: https://developers.tassyir.io/errors/hosted-call-refused HTTP 401, from every operation of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/hosted-call-refused`; its `title`: Hosted call refused. ## What to do HTTP 401. A hosted app's call through `ctx.props.TASSYIR` was refused: its installation is uninstalled or suspended, or no longer runs a released version. It is also the answer during an `app/uninstalled` or `business/redact` delivery. Make the call from the request that received `ctx.props.TASSYIR`; if the installation is active, retry after a short wait ([Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api)). The answer's `fix`: Call the Admin API through ctx.props.TASSYIR.fetch from the request that received it; if the installation is still active, retry after a short wait. ## Answered by Every operation of the Admin API, with HTTP 401. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # HOSTED_PATH_NOT_ALLOWED Source: https://developers.tassyir.io/errors/hosted-path-not-allowed HTTP 400, from a hosted app's TASSYIR binding, before the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/hosted-path-not-allowed`; its `title`: Not an Admin API path. ## What to do HTTP 400. The call was not to an Admin API path. Call `/apps//…` with segments of letters, digits, `_` and `-` only: no other path, no `//`, no trailing `/`, no percent-encoding and no `;`. The binding also reads Tassyir's two public key documents, with `GET` at exactly `/apps/webhook-keys` and `/apps/.well-known/jwks.json` (another method there is `METHOD_NOT_ALLOWED`). ## Answered by A hosted app's `ctx.props.TASSYIR` binding answers it itself, before the call reaches the Admin API, so the problem has no `requestId` and the API's reference does not list it. How the binding calls the API: [Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api). The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # HTML_NOT_ALLOWED Source: https://developers.tassyir.io/errors/html-not-allowed HTTP 400, from 4 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/html-not-allowed`; its `title`: Markup not allowed. ## What to do Send plain formatted HTML (paragraphs, headings, lists, links, Tassyir CDN images) without the parts listed in problems. ## Answered by HTTP 400, from: * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # IDEMPOTENCY_KEY_IN_USE Source: https://developers.tassyir.io/errors/idempotency-key-in-use HTTP 409, from 28 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/idempotency-key-in-use`; its `title`: Idempotency-Key in use. ## What to do HTTP 409. A request with the same key is still running. Wait, then retry with the same key: you will get its answer. The answer's `fix`: Wait for the first request with this Idempotency-Key to finish, then retry. ## Answered by HTTP 409, from: * [`DELETE /collections/{id}`](/api-reference/admin/collections/delete-a-collection) * [`DELETE /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/delete-a-value) * [`DELETE /offers/{id}`](/api-reference/admin/offers/delete-or-archive-an-offer) * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/codes/{codeId}/disable`](/api-reference/admin/offers/disable-a-code) * [`POST /offers/{id}/codes/{codeId}/enable`](/api-reference/admin/offers/enable-a-disabled-code) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/comments`](/api-reference/admin/orders/comment-on-an-order) * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # IDEMPOTENCY_KEY_INVALID Source: https://developers.tassyir.io/errors/idempotency-key-invalid HTTP 400, from 28 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/idempotency-key-invalid`; its `title`: Invalid Idempotency-Key. ## What to do Send 1 to 255 visible ASCII characters, for example a UUID. ## Answered by HTTP 400, from: * [`DELETE /collections/{id}`](/api-reference/admin/collections/delete-a-collection) * [`DELETE /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/delete-a-value) * [`DELETE /offers/{id}`](/api-reference/admin/offers/delete-or-archive-an-offer) * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/codes/{codeId}/disable`](/api-reference/admin/offers/disable-a-code) * [`POST /offers/{id}/codes/{codeId}/enable`](/api-reference/admin/offers/enable-a-disabled-code) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/comments`](/api-reference/admin/orders/comment-on-an-order) * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) A hosted app's `ctx.props.TASSYIR` binding also answers it itself, before the call is sent ([Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api)). The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # IDEMPOTENCY_KEY_REUSED Source: https://developers.tassyir.io/errors/idempotency-key-reused HTTP 422, from 28 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/idempotency-key-reused`; its `title`: Idempotency-Key reused. ## What to do HTTP 422. You reused an `Idempotency-Key` with another method, path, query or body. Use a new key for a different write. The answer's `fix`: Use a new Idempotency-Key for a different request; reuse a key only to retry the identical request. ## Answered by HTTP 422, from: * [`DELETE /collections/{id}`](/api-reference/admin/collections/delete-a-collection) * [`DELETE /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/delete-a-value) * [`DELETE /offers/{id}`](/api-reference/admin/offers/delete-or-archive-an-offer) * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/codes/{codeId}/disable`](/api-reference/admin/offers/disable-a-code) * [`POST /offers/{id}/codes/{codeId}/enable`](/api-reference/admin/offers/enable-a-disabled-code) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/comments`](/api-reference/admin/orders/comment-on-an-order) * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # IDEMPOTENCY_UNAVAILABLE Source: https://developers.tassyir.io/errors/idempotency-unavailable HTTP 503, from 28 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/idempotency-unavailable`; its `title`: Idempotency store unavailable. ## What to do HTTP 503. Tassyir cannot check idempotency keys right now, so the write was not attempted. Retry after `Retry-After` with the same key. The answer's `fix`: Retry the same request with the same Idempotency-Key in a few seconds. ## Answered by HTTP 503, from: * [`DELETE /collections/{id}`](/api-reference/admin/collections/delete-a-collection) * [`DELETE /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/delete-a-value) * [`DELETE /offers/{id}`](/api-reference/admin/offers/delete-or-archive-an-offer) * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/codes/{codeId}/disable`](/api-reference/admin/offers/disable-a-code) * [`POST /offers/{id}/codes/{codeId}/enable`](/api-reference/admin/offers/enable-a-disabled-code) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/comments`](/api-reference/admin/orders/comment-on-an-order) * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # IMAGE_FETCH_FAILED Source: https://developers.tassyir.io/errors/image-fetch-failed HTTP 400, from POST /images. Its problem `type`: `https://developers.tassyir.io/errors/image-fetch-failed`; its `title`: Image could not be downloaded. ## What to do Check that the address opens the image from the public internet over https with a valid certificate; retry later if the server was down. ## Answered by HTTP 400, from: * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # IMAGE_QUOTA_REACHED Source: https://developers.tassyir.io/errors/image-quota-reached HTTP 429, from POST /images. Its problem `type`: `https://developers.tassyir.io/errors/image-quota-reached`; its `title`: Image upload quota reached. ## What to do Wait the number of seconds in Retry-After, then upload the rest more slowly. ## Answered by HTTP 429, from: * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # IMAGE_TOO_LARGE Source: https://developers.tassyir.io/errors/image-too-large HTTP 400, from POST /images. Its problem `type`: `https://developers.tassyir.io/errors/image-too-large`; its `title`: Image too large. ## What to do Send a smaller image: at most 10 MB, 12,000 pixels on a side and 100 megapixels. ## Answered by HTTP 400, from: * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # IMAGE_TYPE_NOT_ALLOWED Source: https://developers.tassyir.io/errors/image-type-not-allowed HTTP 400, from POST /images. Its problem `type`: `https://developers.tassyir.io/errors/image-type-not-allowed`; its `title`: Not a supported image. ## What to do Send a JPEG, PNG, GIF or WebP image; convert an SVG or any other format to one of these first. ## Answered by HTTP 400, from: * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # IMAGE_URL_NOT_ALLOWED Source: https://developers.tassyir.io/errors/image-url-not-allowed HTTP 400, from POST /images. Its problem `type`: `https://developers.tassyir.io/errors/image-url-not-allowed`; its `title`: Image address not allowed. ## What to do Send the https address (port 443, no user or password) of the image on a public web server, by its host name. ## Answered by HTTP 400, from: * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # INSUFFICIENT_AT_LOCATION Source: https://developers.tassyir.io/errors/insufficient-at-location HTTP 409, from POST /inventory-levels/{id}/adjust. Its problem `type`: `https://developers.tassyir.io/errors/insufficient-at-location`; its `title`: Conflict. ## What to do Read the level again and remove at most what it holds: stock never goes below zero. ## Answered by HTTP 409, from: * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # INTERNAL_ERROR Source: https://developers.tassyir.io/errors/internal-error HTTP 500, from every operation of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/internal-error`; its `title`: Internal error. ## What to do HTTP 500. Tassyir's fault. Retry reads with backoff; retry writes only with the same `Idempotency-Key`. Quote the `requestId` if it persists. The answer's `fix`: Retry with exponential backoff; if it keeps failing, report the requestId to Tassyir. ## Answered by Every operation of the Admin API, with HTTP 500. Also the code of a 5xx failure with a status other than 503, when it carries no more specific code. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # INVALID_CURSOR Source: https://developers.tassyir.io/errors/invalid-cursor HTTP 400, from 13 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/invalid-cursor`; its `title`: Invalid cursor. ## What to do HTTP 400. The `cursor` is not this list's, was edited, or was made with other filters. Send `nextCursor` back unchanged, with the same filters; to change a filter, start again without a cursor. The answer's `fix`: Pass the nextCursor of the previous page unchanged with the same filters, or start again without a cursor. ## Answered by HTTP 400, from: * [`GET /bulk/exports`](/api-reference/admin/bulk-exports/list-the-installations-exports) * [`GET /collections`](/api-reference/admin/collections/list-collections) * [`GET /customers`](/api-reference/admin/customers/list-customers) * [`GET /data`](/api-reference/admin/data/list-the-apps-values) * [`GET /delivery-agents`](/api-reference/admin/delivery/list-delivery-agents) * [`GET /inventory-levels`](/api-reference/admin/inventory/list-stock-levels) * [`GET /offers`](/api-reference/admin/offers/list-offers) * [`GET /offers/{id}/codes`](/api-reference/admin/offers/list-an-offers-codes) * [`GET /offers/{id}/redemptions`](/api-reference/admin/offers/list-an-offers-redemptions) * [`GET /orders`](/api-reference/admin/orders/list-orders) * [`GET /orders/{id}/comments`](/api-reference/admin/orders/list-an-orders-comments) * [`GET /products`](/api-reference/admin/products/list-products) * [`GET /stores`](/api-reference/admin/stores/list-stores) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # INVALID_FIELDS Source: https://developers.tassyir.io/errors/invalid-fields HTTP 400, from 49 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/invalid-fields`; its `title`: Unknown fields. ## What to do HTTP 400. `fields` names a field the resource does not have. `allowedFields` lists the valid names; fields are top-level only. The answer's `fix`: Name only fields listed in allowedFields, separated by commas. ## Answered by HTTP 400, from: * [`GET /bulk/exports`](/api-reference/admin/bulk-exports/list-the-installations-exports) * [`GET /bulk/exports/{id}`](/api-reference/admin/bulk-exports/get-an-export) * [`GET /business`](/api-reference/admin/business/the-business-the-app-is-installed-on) * [`GET /cancel-reasons`](/api-reference/admin/orders/the-merchants-cancel-reasons) * [`GET /collections`](/api-reference/admin/collections/list-collections) * [`GET /collections/{id}`](/api-reference/admin/collections/get-a-collection) * [`GET /customers`](/api-reference/admin/customers/list-customers) * [`GET /customers/{id}`](/api-reference/admin/customers/get-a-customer) * [`GET /data`](/api-reference/admin/data/list-the-apps-values) * [`GET /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/get-a-value) * [`GET /delivery-agents`](/api-reference/admin/delivery/list-delivery-agents) * [`GET /delivery-agents/{id}`](/api-reference/admin/delivery/get-a-delivery-agent) * [`GET /delivery-fee-groups`](/api-reference/admin/delivery/list-delivery-fee-groups) * [`GET /delivery-fee-groups/{id}`](/api-reference/admin/delivery/get-a-delivery-fee-group) * [`GET /inventory-levels`](/api-reference/admin/inventory/list-stock-levels) * [`GET /inventory-levels/{id}`](/api-reference/admin/inventory/get-a-stock-level) * [`GET /offers`](/api-reference/admin/offers/list-offers) * [`GET /offers/{id}`](/api-reference/admin/offers/get-an-offer) * [`GET /offers/{id}/codes`](/api-reference/admin/offers/list-an-offers-codes) * [`GET /offers/{id}/redemptions`](/api-reference/admin/offers/list-an-offers-redemptions) * [`GET /orders`](/api-reference/admin/orders/list-orders) * [`GET /orders/{id}`](/api-reference/admin/orders/get-an-order) * [`GET /orders/{id}/comments`](/api-reference/admin/orders/list-an-orders-comments) * [`GET /products`](/api-reference/admin/products/list-products) * [`GET /products/{id}`](/api-reference/admin/products/get-a-product) * [`GET /stores`](/api-reference/admin/stores/list-stores) * [`GET /stores/{id}`](/api-reference/admin/stores/get-a-store) * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/{codeId}/disable`](/api-reference/admin/offers/disable-a-code) * [`POST /offers/{id}/codes/{codeId}/enable`](/api-reference/admin/offers/enable-a-disabled-code) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/comments`](/api-reference/admin/orders/comment-on-an-order) * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # LOCATION_NOT_FOUND Source: https://developers.tassyir.io/errors/location-not-found HTTP 400, 404, from POST /inventory-levels/{id}/adjust. Its problem `type`: `https://developers.tassyir.io/errors/location-not-found`; its `title`: Bad request. ## What to do Name an active location of this business, or leave locationId out to use the default location. ## Answered by HTTP 400, 404, from: * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # MEDIA_URL_NOT_ALLOWED Source: https://developers.tassyir.io/errors/media-url-not-allowed HTTP 400, from 5 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/media-url-not-allowed`; its `title`: Image address not allowed. ## What to do HTTP 400. An image is not on `https://cdn.tassyir.io/` and not already on the resource (`field`, `url`). Upload it with `POST /images` first ([Products and collections](/guides/products-and-collections#bring-an-image-onto-the-cdn)). The answer's `fix`: Upload the image with POST /images and use the URL it answers ([https://cdn.tassyir.io/…](https://cdn.tassyir.io/…)), or keep one this resource already shows; do not link images hosted elsewhere. ## Answered by HTTP 400, from: * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # METHOD_NOT_ALLOWED Source: https://developers.tassyir.io/errors/method-not-allowed A refusal with HTTP 405 that carries no more specific code. Its problem `type`: `https://developers.tassyir.io/errors/method-not-allowed`; its `title`: Method not allowed. ## What to do Call the operation with the method its reference names. ## Answered by The code of a refusal with HTTP 405 that carries no more specific code. Any operation may answer it. A hosted app's `ctx.props.TASSYIR` binding also answers it itself, before the call is sent ([Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api)). The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # NOT_FOUND Source: https://developers.tassyir.io/errors/not-found HTTP 404, from 40 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/not-found`; its `title`: Not found. ## What to do HTTP 404. No such route, or no resource your token can read under that id: a resource of another store or business, or a malformed id, reads the same. The answer's `fix`: Check the path and the id; a resource outside this installation's stores reads as not found. ## Answered by HTTP 404, from: * [`DELETE /collections/{id}`](/api-reference/admin/collections/delete-a-collection) * [`DELETE /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/delete-a-value) * [`DELETE /offers/{id}`](/api-reference/admin/offers/delete-or-archive-an-offer) * [`GET /bulk/exports/{id}`](/api-reference/admin/bulk-exports/get-an-export) * [`GET /business`](/api-reference/admin/business/the-business-the-app-is-installed-on) * [`GET /collections/{id}`](/api-reference/admin/collections/get-a-collection) * [`GET /customers/{id}`](/api-reference/admin/customers/get-a-customer) * [`GET /data`](/api-reference/admin/data/list-the-apps-values) * [`GET /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/get-a-value) * [`GET /delivery-agents/{id}`](/api-reference/admin/delivery/get-a-delivery-agent) * [`GET /delivery-fee-groups/{id}`](/api-reference/admin/delivery/get-a-delivery-fee-group) * [`GET /inventory-levels/{id}`](/api-reference/admin/inventory/get-a-stock-level) * [`GET /offers/{id}`](/api-reference/admin/offers/get-an-offer) * [`GET /offers/{id}/codes`](/api-reference/admin/offers/list-an-offers-codes) * [`GET /offers/{id}/redemptions`](/api-reference/admin/offers/list-an-offers-redemptions) * [`GET /orders/{id}`](/api-reference/admin/orders/get-an-order) * [`GET /orders/{id}/comments`](/api-reference/admin/orders/list-an-orders-comments) * [`GET /products/{id}`](/api-reference/admin/products/get-a-product) * [`GET /stores/{id}`](/api-reference/admin/stores/get-a-store) * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/codes/{codeId}/disable`](/api-reference/admin/offers/disable-a-code) * [`POST /offers/{id}/codes/{codeId}/enable`](/api-reference/admin/offers/enable-a-disabled-code) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/comments`](/api-reference/admin/orders/comment-on-an-order) * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) Also the code of a refusal with HTTP 404 that carries no more specific code. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_ARCHIVED Source: https://developers.tassyir.io/errors/offer-archived HTTP 409, from 6 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-archived`; its `title`: Conflict. ## What to do Create a new offer: an archived one is history and cannot be changed. ## Answered by HTTP 409, from: * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_CODE_INVALID Source: https://developers.tassyir.io/errors/offer-code-invalid HTTP 400, from POST /offers/{id}/codes. Its problem `type`: `https://developers.tassyir.io/errors/offer-code-invalid`; its `title`: Bad request. ## What to do Send a code of 3 to 30 characters: letters, digits, hyphens and underscores. ## Answered by HTTP 400, from: * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_CODE_NOT_FOUND Source: https://developers.tassyir.io/errors/offer-code-not-found HTTP 404, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-code-not-found`; its `title`: Not found. ## What to do Name a code of this offer (GET /offers/\{id}/codes). ## Answered by HTTP 404, from: * [`POST /offers/{id}/codes/{codeId}/disable`](/api-reference/admin/offers/disable-a-code) * [`POST /offers/{id}/codes/{codeId}/enable`](/api-reference/admin/offers/enable-a-disabled-code) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_CODE_TAKEN Source: https://developers.tassyir.io/errors/offer-code-taken HTTP 409, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-code-taken`; its `title`: Conflict. ## What to do Choose another code: codes are unique across the whole business, disabled ones included. ## Answered by HTTP 409, from: * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_ENDED Source: https://developers.tassyir.io/errors/offer-ended HTTP 409, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-ended`; its `title`: Conflict. ## What to do Move endsAt to a later date with PATCH /offers/\{id}: an ended offer is not resumed. ## Answered by HTTP 409, from: * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_GONE Source: https://developers.tassyir.io/errors/offer-gone HTTP 409, from POST /orders. Its problem `type`: `https://developers.tassyir.io/errors/offer-gone`; its `title`: Conflict. ## What to do Send the order again: the offer behind the code was deleted, and the retry will list the code in rejectedCodes and create the order without it. ## Answered by HTTP 409, from: * [`POST /orders`](/api-reference/admin/orders/create-an-order) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_INVALID Source: https://developers.tassyir.io/errors/offer-invalid HTTP 400, from 3 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-invalid`; its `title`: Bad request. ## What to do Correct the fields listed in errors (each has a field and a code), then send the request again. ## Answered by HTTP 400, from: * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_KIND_IMMUTABLE Source: https://developers.tassyir.io/errors/offer-kind-immutable HTTP 400, from PATCH /offers/{id}. Its problem `type`: `https://developers.tassyir.io/errors/offer-kind-immutable`; its `title`: Bad request. ## What to do Leave kind and method as the offer has them: create a new offer to change either. ## Answered by HTTP 400, from: * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_KIND_NOT_AVAILABLE Source: https://developers.tassyir.io/errors/offer-kind-not-available HTTP 400, from POST /offers. Its problem `type`: `https://developers.tassyir.io/errors/offer-kind-not-available`; its `title`: Bad request. ## What to do Create an offer of a kind the dashboard offers today; the other kinds in the model open later. ## Answered by HTTP 400, from: * [`POST /offers`](/api-reference/admin/offers/create-an-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_LIMIT_REACHED Source: https://developers.tassyir.io/errors/offer-limit-reached HTTP 409, from POST /orders. Its problem `type`: `https://developers.tassyir.io/errors/offer-limit-reached`; its `title`: Conflict. ## What to do Send the order again: the code (or offer) has no uses left, and the retry will list the code in rejectedCodes as code\_used\_up and create the order without it. ## Answered by HTTP 409, from: * [`POST /orders`](/api-reference/admin/orders/create-an-order) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_NOT_CODE_METHOD Source: https://developers.tassyir.io/errors/offer-not-code-method HTTP 409, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-not-code-method`; its `title`: Conflict. ## What to do Only an offer created with method "code" has codes; create a code offer to use them. ## Answered by HTTP 409, from: * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_NOT_FOUND Source: https://developers.tassyir.io/errors/offer-not-found HTTP 404, from 9 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-not-found`; its `title`: Not found. ## What to do The offer was deleted between the check and the change; read the list again. ## Answered by HTTP 404, from: * [`DELETE /offers/{id}`](/api-reference/admin/offers/delete-or-archive-an-offer) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/codes/{codeId}/disable`](/api-reference/admin/offers/disable-a-code) * [`POST /offers/{id}/codes/{codeId}/enable`](/api-reference/admin/offers/enable-a-disabled-code) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_NOT_OWNED Source: https://developers.tassyir.io/errors/offer-not-owned HTTP 403, from 11 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-not-owned`; its `title`: Offer not owned by this app. ## What to do HTTP 403. You tried to change, or read the codes or redemptions of, an offer your installation did not create: the merchant's own, or another app's. An app writes only its own offers. Create your own offer with `POST /offers` and change that one ([Write offers and codes](/guides/write-offers#you-write-only-the-offers-you-own)). The merchant's dashboard has a counterpart you never get: editing or duplicating an **app-owned** offer there answers 409 `OFFER_APP_OWNED`, because such an offer is changed in the app that owns it. The answer's `fix`: Create your own offer with POST /offers and change that one; the merchant edits their own offers in the dashboard. ## Answered by HTTP 403, from: * [`DELETE /offers/{id}`](/api-reference/admin/offers/delete-or-archive-an-offer) * [`GET /offers/{id}/codes`](/api-reference/admin/offers/list-an-offers-codes) * [`GET /offers/{id}/redemptions`](/api-reference/admin/offers/list-an-offers-redemptions) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/codes/{codeId}/disable`](/api-reference/admin/offers/disable-a-code) * [`POST /offers/{id}/codes/{codeId}/enable`](/api-reference/admin/offers/enable-a-disabled-code) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_NOT_PUBLISHED Source: https://developers.tassyir.io/errors/offer-not-published HTTP 409, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-not-published`; its `title`: Conflict. ## What to do Publish the draft with POST /offers/\{id}/publish first. ## Answered by HTTP 409, from: * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_STORE_NOT_FOUND Source: https://developers.tassyir.io/errors/offer-store-not-found HTTP 404, from POST /offers. Its problem `type`: `https://developers.tassyir.io/errors/offer-store-not-found`; its `title`: Not found. ## What to do Send the id of a store of this business that the installation may access (GET /stores). ## Answered by HTTP 404, from: * [`POST /offers`](/api-reference/admin/offers/create-an-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_TARGET_NOT_FOUND Source: https://developers.tassyir.io/errors/offer-target-not-found HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-target-not-found`; its `title`: Bad request. ## What to do Target only products, variants and collections of this business that still exist; the ids in the offer's targets are not. ## Answered by HTTP 400, from: * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_TARGET_OUTSIDE_STORE Source: https://developers.tassyir.io/errors/offer-target-outside-store HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-target-outside-store`; its `title`: Bad request. ## What to do Target only products, variants and collections of the offer's store. ## Answered by HTTP 400, from: * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # OFFER_VERSION_CONFLICT Source: https://developers.tassyir.io/errors/offer-version-conflict HTTP 409, from 4 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/offer-version-conflict`; its `title`: Conflict. ## What to do Read the offer again (currentVersion is its version now) and send expectedVersion with the change, or leave expectedVersion out. ## Answered by HTTP 409, from: * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # ORDER_DUPLICATE Source: https://developers.tassyir.io/errors/order-duplicate HTTP 409, from POST /orders. Its problem `type`: `https://developers.tassyir.io/errors/order-duplicate`; its `title`: Duplicate order. ## What to do Do not retry: the buyer already has this order, which the merchant handles. ## Answered by HTTP 409, from: * [`POST /orders`](/api-reference/admin/orders/create-an-order) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # ORDER_LIMIT_REACHED Source: https://developers.tassyir.io/errors/order-limit-reached HTTP 403, from POST /orders/{id}/confirm. Its problem `type`: `https://developers.tassyir.io/errors/order-limit-reached`; its `title`: Order waiting for the plan allowance. ## What to do HTTP 403. The order is waiting: it is past the plan's order allowance, and cannot be confirmed until it unlocks (`orders/updated` with `waiting`). The answer's `fix`: Retry after the merchant upgrades or the plan's next order cycle starts. ## Answered by HTTP 403, from: * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # ORDER_NOT_EDITABLE Source: https://developers.tassyir.io/errors/order-not-editable HTTP 409, from 4 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/order-not-editable`; its `title`: Order no longer editable. ## What to do HTTP 409. The order is dispatched, delivered or returned (`orderStatus`): it can no longer be edited, confirmed or cancelled. The answer's `fix`: Leave this order as it is: once with a carrier it changes only through the delivery. ## Answered by HTTP 409, from: * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # ORDER_PRICING_LOCKED Source: https://developers.tassyir.io/errors/order-pricing-locked HTTP 400, from PATCH /orders/{id}. Its problem `type`: `https://developers.tassyir.io/errors/order-pricing-locked`; its `title`: Bad request. ## What to do Leave the wilaya, commune and delivery as they are: the order is with a carrier, delivered or returned, so its price no longer changes. ## Answered by HTTP 400, from: * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # ORDER_STATUS_CONFLICT Source: https://developers.tassyir.io/errors/order-status-conflict HTTP 409, from 3 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/order-status-conflict`; its `title`: Status change not allowed. ## What to do HTTP 409. The order's status does not allow this move (`orderStatus`, `requestedStatus`): read the order again and decide. The answer's `fix`: Read the order again and act on its current status; do not retry the same move. ## Answered by HTTP 409, from: * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PAYLOAD_TOO_LARGE Source: https://developers.tassyir.io/errors/payload-too-large HTTP 413, from 22 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/payload-too-large`; its `title`: Payload too large. ## What to do Send a smaller body: split the write into several requests. ## Answered by HTTP 413, from: * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/comments`](/api-reference/admin/orders/comment-on-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) Also the code of a refusal with HTTP 413 that carries no more specific code. A hosted app's `ctx.props.TASSYIR` binding also answers it itself, before the call is sent ([Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api)). The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PERMISSION_DENIED Source: https://developers.tassyir.io/errors/permission-denied HTTP 403, from 54 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/permission-denied`; its `title`: Permission denied. ## What to do HTTP 403. Your installation lacks the permission the route checks (`permission`). The `fix` names the scope: add it to the manifest, upload a version, and the merchant must accept the update. With an online token, the person may lack it even when your app has it. ## Answered by HTTP 403, from: * [`DELETE /collections/{id}`](/api-reference/admin/collections/delete-a-collection) * [`DELETE /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/delete-a-value) * [`DELETE /offers/{id}`](/api-reference/admin/offers/delete-or-archive-an-offer) * [`GET /bulk/exports`](/api-reference/admin/bulk-exports/list-the-installations-exports) * [`GET /bulk/exports/{id}`](/api-reference/admin/bulk-exports/get-an-export) * [`GET /cancel-reasons`](/api-reference/admin/orders/the-merchants-cancel-reasons) * [`GET /collections`](/api-reference/admin/collections/list-collections) * [`GET /collections/{id}`](/api-reference/admin/collections/get-a-collection) * [`GET /customers`](/api-reference/admin/customers/list-customers) * [`GET /customers/{id}`](/api-reference/admin/customers/get-a-customer) * [`GET /data`](/api-reference/admin/data/list-the-apps-values) * [`GET /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/get-a-value) * [`GET /delivery-agents`](/api-reference/admin/delivery/list-delivery-agents) * [`GET /delivery-agents/{id}`](/api-reference/admin/delivery/get-a-delivery-agent) * [`GET /delivery-fee-groups`](/api-reference/admin/delivery/list-delivery-fee-groups) * [`GET /delivery-fee-groups/{id}`](/api-reference/admin/delivery/get-a-delivery-fee-group) * [`GET /inventory-levels`](/api-reference/admin/inventory/list-stock-levels) * [`GET /inventory-levels/{id}`](/api-reference/admin/inventory/get-a-stock-level) * [`GET /offers`](/api-reference/admin/offers/list-offers) * [`GET /offers/{id}`](/api-reference/admin/offers/get-an-offer) * [`GET /offers/{id}/codes`](/api-reference/admin/offers/list-an-offers-codes) * [`GET /offers/{id}/redemptions`](/api-reference/admin/offers/list-an-offers-redemptions) * [`GET /orders`](/api-reference/admin/orders/list-orders) * [`GET /orders/{id}`](/api-reference/admin/orders/get-an-order) * [`GET /orders/{id}/comments`](/api-reference/admin/orders/list-an-orders-comments) * [`GET /products`](/api-reference/admin/products/list-products) * [`GET /products/{id}`](/api-reference/admin/products/get-a-product) * [`GET /stores`](/api-reference/admin/stores/list-stores) * [`GET /stores/{id}`](/api-reference/admin/stores/get-a-store) * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/codes/{codeId}/disable`](/api-reference/admin/offers/disable-a-code) * [`POST /offers/{id}/codes/{codeId}/enable`](/api-reference/admin/offers/enable-a-disabled-code) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/comments`](/api-reference/admin/orders/comment-on-an-order) * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PLAN_FEATURE_REQUIRED Source: https://developers.tassyir.io/errors/plan-feature-required HTTP 403, from 3 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/plan-feature-required`; its `title`: Not included in the merchant's plan. ## What to do HTTP 403. The merchant's plan lacks the feature (`feature`), such as stock at a chosen location. Do without it, or tell the merchant. The answer's `fix`: The merchant's plan does not include this feature; it works once the merchant upgrades. ## Answered by HTTP 403, from: * [`GET /inventory-levels`](/api-reference/admin/inventory/list-stock-levels) * [`GET /inventory-levels/{id}`](/api-reference/admin/inventory/get-a-stock-level) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PLAN_LIMIT_REACHED Source: https://developers.tassyir.io/errors/plan-limit-reached HTTP 403, from 3 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/plan-limit-reached`; its `title`: Merchant's plan limit reached. ## What to do The merchant's plan limit is reached; it works once the merchant upgrades or frees capacity. ## Answered by HTTP 403, from: * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_DEFAULT_VARIANT Source: https://developers.tassyir.io/errors/product-default-variant HTTP 409, from PATCH /products/{id}/variants/{variantId}. Its problem `type`: `https://developers.tassyir.io/errors/product-default-variant`; its `title`: Default variant follows its product. ## What to do Change the product's own fields with PATCH /products/\{id}; its default variant follows them. ## Answered by HTTP 409, from: * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_OPTION_DUPLICATE_NAME Source: https://developers.tassyir.io/errors/product-option-duplicate-name HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-option-duplicate-name`; its `title`: Bad request. ## What to do Give each option a different name (case and surrounding spaces are ignored when comparing). ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_OPTION_DUPLICATE_VALUE Source: https://developers.tassyir.io/errors/product-option-duplicate-value HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-option-duplicate-value`; its `title`: Bad request. ## What to do List each value of an option once (case and surrounding spaces are ignored when comparing). ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_OPTION_NO_VALUES Source: https://developers.tassyir.io/errors/product-option-no-values HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-option-no-values`; its `title`: Bad request. ## What to do Give every option at least one value that is not empty. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_OPTION_NOT_FOUND Source: https://developers.tassyir.io/errors/product-option-not-found HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-option-not-found`; its `title`: Bad request. ## What to do Name in a variant's options only options the product has, spelled as in options. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_OPTION_VALUE_EMPTY Source: https://developers.tassyir.io/errors/product-option-value-empty HTTP 400, from PATCH /products/{id}. Its problem `type`: `https://developers.tassyir.io/errors/product-option-value-empty`; its `title`: Bad request. ## What to do Remove the empty value: every option value needs at least one visible character. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_OPTION_VALUE_NOT_FOUND Source: https://developers.tassyir.io/errors/product-option-value-not-found HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-option-value-not-found`; its `title`: Bad request. ## What to do Select in each variant only values its option lists in options. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_OPTIONS_REQUIRE_VARIANTS Source: https://developers.tassyir.io/errors/product-options-require-variants HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-options-require-variants`; its `title`: Bad request. ## What to do Send variants with the options: one for each combination of their values. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_SLUG_INVALID Source: https://developers.tassyir.io/errors/product-slug-invalid HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-slug-invalid`; its `title`: Bad request. ## What to do Send a slug with at least one letter that is not a reserved word, or leave it out to derive it from the name. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_SLUG_TAKEN Source: https://developers.tassyir.io/errors/product-slug-taken HTTP 409, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-slug-taken`; its `title`: Conflict. ## What to do Send another slug, or leave it out: another product of this store already uses this one. ## Answered by HTTP 409, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_VARIANT_COMBINATION_MISMATCH Source: https://developers.tassyir.io/errors/product-variant-combination-mismatch HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-variant-combination-mismatch`; its `title`: Bad request. ## What to do Send exactly one variant for each combination of option values, none twice. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_VARIANT_COUNT_MISMATCH Source: https://developers.tassyir.io/errors/product-variant-count-mismatch HTTP 400, from 3 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-variant-count-mismatch`; its `title`: Bad request. ## What to do Send one variant per combination of option values: as many variants as the product of the options' value counts. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_VARIANT_DUPLICATE_COMBINATION Source: https://developers.tassyir.io/errors/product-variant-duplicate-combination HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-variant-duplicate-combination`; its `title`: Bad request. ## What to do Give each variant a different combination of option values. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_VARIANT_DUPLICATE_OPTION Source: https://developers.tassyir.io/errors/product-variant-duplicate-option HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-variant-duplicate-option`; its `title`: Bad request. ## What to do Name each option once in a variant's options. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_VARIANT_INVALID_VALUE Source: https://developers.tassyir.io/errors/product-variant-invalid-value HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-variant-invalid-value`; its `title`: Bad request. ## What to do Select for each option one of the values it lists in options. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_VARIANT_MISSING_COMBINATIONS Source: https://developers.tassyir.io/errors/product-variant-missing-combinations HTTP 400, from 3 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-variant-missing-combinations`; its `title`: Bad request. ## What to do Add a variant for every combination of option values that has none. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_VARIANT_MISSING_OPTION Source: https://developers.tassyir.io/errors/product-variant-missing-option HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-variant-missing-option`; its `title`: Bad request. ## What to do Select a value of every option in each variant, none left out. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_VARIANT_SELECTED_COUNT Source: https://developers.tassyir.io/errors/product-variant-selected-count HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-variant-selected-count`; its `title`: Bad request. ## What to do Select exactly one value of every option in each variant. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_VARIANT_UNKNOWN_OPTION Source: https://developers.tassyir.io/errors/product-variant-unknown-option HTTP 400, from 2 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-variant-unknown-option`; its `title`: Bad request. ## What to do Name in a variant's options only options the product has. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # PRODUCT_WRITE_TIMEOUT Source: https://developers.tassyir.io/errors/product-write-timeout HTTP 400, from 3 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/product-write-timeout`; its `title`: Bad request. ## What to do Nothing was saved: retry the same request, and if it times out again, save fewer options or values at once. ## Answered by HTTP 400, from: * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /products`](/api-reference/admin/products/create-a-product) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # RATE_LIMITED Source: https://developers.tassyir.io/errors/rate-limited HTTP 429, from every operation of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/rate-limited`; its `title`: Rate limit exceeded. ## What to do HTTP 429. Your installation's bucket is empty. Wait `Retry-After` seconds, then retry; spread your calls using the `RateLimit-*` headers. The answer's `fix`: Wait the number of seconds in Retry-After, then retry; keep an eye on RateLimit-Remaining to spread requests. ## Answered by Every operation of the Admin API, with HTTP 429. Also the code of a refusal with HTTP 429 that carries no more specific code. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # REQUEST_FAILED Source: https://developers.tassyir.io/errors/request-failed A 4xx refusal with a status other than 400, 401, 403, 404, 405, 409, 413, 415, 422, 429 or 460, when it carries no more specific code. Its problem `type`: `https://developers.tassyir.io/errors/request-failed`; its `title`: Request failed. ## What to do Act on the status and the detail; if they do not say enough, report the requestId to Tassyir. ## Answered by The code of a 4xx refusal with a status other than 400, 401, 403, 404, 405, 409, 413, 415, 422, 429 or 460, when it carries no more specific code. Any operation may answer it. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # RESPONSE_TOO_LARGE Source: https://developers.tassyir.io/errors/response-too-large HTTP 502, from a hosted app's TASSYIR binding, before the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/response-too-large`; its `title`: Response too large. ## What to do HTTP 502. The API's answer was larger than a hosted app may receive (8 MiB). Ask for less: a smaller `limit`, or `?fields=` with only the fields you read. ## Answered by A hosted app's `ctx.props.TASSYIR` binding answers it itself, before the call reaches the Admin API, so the problem has no `requestId` and the API's reference does not list it. How the binding calls the API: [Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api). The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # SERVICE_UNAVAILABLE Source: https://developers.tassyir.io/errors/service-unavailable HTTP 503, from every operation of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/service-unavailable`; its `title`: Service unavailable. ## What to do HTTP 503. Something Tassyir depends on is down. Retry with backoff, writes with the same `Idempotency-Key`. The answer's `fix`: Retry with exponential backoff. ## Answered by Every operation of the Admin API, with HTTP 503. Also the code of a refusal with HTTP 503 that carries no more specific code. A hosted app's `ctx.props.TASSYIR` binding also answers it itself, before the call is sent ([Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api)). The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # STORE_REQUIRED Source: https://developers.tassyir.io/errors/store-required HTTP 400, from POST /orders. Its problem `type`: `https://developers.tassyir.io/errors/store-required`; its `title`: Bad request. ## What to do Name the store with storeId: this installation acts in several stores. ## Answered by HTTP 400, from: * [`POST /orders`](/api-reference/admin/orders/create-an-order) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # STORE_SCOPE_FORBIDDEN Source: https://developers.tassyir.io/errors/store-scope-forbidden HTTP 460, from 17 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/store-scope-forbidden`; its `title`: Store not accessible. ## What to do HTTP 460. Your request named a store (`storeId`) outside the stores the merchant gave your installation. Use one of its stores; list them with `GET /stores` (`read_stores`). The answer's `fix`: Only name stores this installation may access; the merchant chooses them when installing the app. ## Answered by HTTP 460, from: * [`DELETE /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/delete-a-value) * [`GET /collections`](/api-reference/admin/collections/list-collections) * [`GET /customers`](/api-reference/admin/customers/list-customers) * [`GET /data`](/api-reference/admin/data/list-the-apps-values) * [`GET /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/get-a-value) * [`GET /delivery-fee-groups`](/api-reference/admin/delivery/list-delivery-fee-groups) * [`GET /inventory-levels`](/api-reference/admin/inventory/list-stock-levels) * [`GET /offers`](/api-reference/admin/offers/list-offers) * [`GET /orders`](/api-reference/admin/orders/list-orders) * [`GET /products`](/api-reference/admin/products/list-products) * [`GET /stores/{id}`](/api-reference/admin/stores/get-a-store) * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) Also the code of a refusal with HTTP 460 that carries no more specific code. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # SUBSCRIPTION_REQUIRED Source: https://developers.tassyir.io/errors/subscription-required HTTP 422, from 3 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/subscription-required`; its `title`: Unprocessable request. ## What to do HTTP 422. The route needs a plan, and the business has no usable subscription. Tell the merchant. The answer's `fix`: Do not retry until the merchant subscribes again: this needs a subscription the business does not have. ## Answered by HTTP 422, from: * [`GET /inventory-levels`](/api-reference/admin/inventory/list-stock-levels) * [`GET /inventory-levels/{id}`](/api-reference/admin/inventory/get-a-stock-level) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # TASSYIR_BINDING_NOT_AVAILABLE Source: https://developers.tassyir.io/errors/tassyir-binding-not-available HTTP 503, from a hosted app's TASSYIR binding, before the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/tassyir-binding-not-available`; its `title`: Not available. ## What to do HTTP 503. Calls to the Admin API from a hosted app are not available right now; nothing was sent. Try again later. ## Answered by A hosted app's `ctx.props.TASSYIR` binding answers it itself, before the call reaches the Admin API, so the problem has no `requestId` and the API's reference does not list it. How the binding calls the API: [Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api). The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # UNAUTHORIZED Source: https://developers.tassyir.io/errors/unauthorized A refusal with HTTP 401 that carries no more specific code. Its problem `type`: `https://developers.tassyir.io/errors/unauthorized`; its `title`: Unauthorized. ## What to do Send a valid access token as Authorization: Bearer tsy\_at\_…; get a new one from POST /apps/oauth/token. ## Answered by The code of a refusal with HTTP 401 that carries no more specific code. Any operation may answer it. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # UNPROCESSABLE_ENTITY Source: https://developers.tassyir.io/errors/unprocessable-entity A refusal with HTTP 422 that carries no more specific code. Its problem `type`: `https://developers.tassyir.io/errors/unprocessable-entity`; its `title`: Unprocessable request. ## What to do Correct the request as the detail says; the same request fails the same way. ## Answered by The code of a refusal with HTTP 422 that carries no more specific code. Any operation may answer it. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # UNSUPPORTED_MEDIA_TYPE Source: https://developers.tassyir.io/errors/unsupported-media-type HTTP 415, from 22 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/unsupported-media-type`; its `title`: Unsupported media type. ## What to do Send the body as JSON, with Content-Type: application/json in UTF-8. ## Answered by HTTP 415, from: * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/comments`](/api-reference/admin/orders/comment-on-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) Also the code of a refusal with HTTP 415 that carries no more specific code. A hosted app's `ctx.props.TASSYIR` binding also answers it itself, before the call is sent ([Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api)). The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # VALIDATION_FAILED Source: https://developers.tassyir.io/errors/validation-failed HTTP 400, from 53 operations of the Admin API. Its problem `type`: `https://developers.tassyir.io/errors/validation-failed`; its `title`: Invalid request. ## What to do HTTP 400. A parameter or a field failed validation, or is not one the route takes. `errors` lists each problem. Fix the request: unknown parameters are refused on purpose, so a misspelt filter never silently returns everything. The answer's `fix`: Correct the parameters listed in errors, then retry. ## Answered by HTTP 400, from: * [`DELETE /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/delete-a-value) * [`GET /bulk/exports`](/api-reference/admin/bulk-exports/list-the-installations-exports) * [`GET /bulk/exports/{id}`](/api-reference/admin/bulk-exports/get-an-export) * [`GET /business`](/api-reference/admin/business/the-business-the-app-is-installed-on) * [`GET /cancel-reasons`](/api-reference/admin/orders/the-merchants-cancel-reasons) * [`GET /collections`](/api-reference/admin/collections/list-collections) * [`GET /collections/{id}`](/api-reference/admin/collections/get-a-collection) * [`GET /customers`](/api-reference/admin/customers/list-customers) * [`GET /customers/{id}`](/api-reference/admin/customers/get-a-customer) * [`GET /data`](/api-reference/admin/data/list-the-apps-values) * [`GET /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/get-a-value) * [`GET /delivery-agents`](/api-reference/admin/delivery/list-delivery-agents) * [`GET /delivery-agents/{id}`](/api-reference/admin/delivery/get-a-delivery-agent) * [`GET /delivery-fee-groups`](/api-reference/admin/delivery/list-delivery-fee-groups) * [`GET /delivery-fee-groups/{id}`](/api-reference/admin/delivery/get-a-delivery-fee-group) * [`GET /inventory-levels`](/api-reference/admin/inventory/list-stock-levels) * [`GET /inventory-levels/{id}`](/api-reference/admin/inventory/get-a-stock-level) * [`GET /offers`](/api-reference/admin/offers/list-offers) * [`GET /offers/{id}`](/api-reference/admin/offers/get-an-offer) * [`GET /offers/{id}/codes`](/api-reference/admin/offers/list-an-offers-codes) * [`GET /offers/{id}/redemptions`](/api-reference/admin/offers/list-an-offers-redemptions) * [`GET /orders`](/api-reference/admin/orders/list-orders) * [`GET /orders/{id}`](/api-reference/admin/orders/get-an-order) * [`GET /orders/{id}/comments`](/api-reference/admin/orders/list-an-orders-comments) * [`GET /products`](/api-reference/admin/products/list-products) * [`GET /products/{id}`](/api-reference/admin/products/get-a-product) * [`GET /stores`](/api-reference/admin/stores/list-stores) * [`GET /stores/{id}`](/api-reference/admin/stores/get-a-store) * [`PATCH /collections/{id}`](/api-reference/admin/collections/update-a-collection) * [`PATCH /collections/{id}/products`](/api-reference/admin/collections/change-a-collections-members) * [`PATCH /offers/{id}`](/api-reference/admin/offers/update-an-offer) * [`PATCH /orders/{id}`](/api-reference/admin/orders/update-an-order) * [`PATCH /products/{id}`](/api-reference/admin/products/update-a-product) * [`PATCH /products/{id}/variants/{variantId}`](/api-reference/admin/products/update-one-variants-fields) * [`POST /bulk/exports`](/api-reference/admin/bulk-exports/export-a-whole-list) * [`POST /collections`](/api-reference/admin/collections/create-a-collection) * [`POST /images`](/api-reference/admin/images/upload-an-image-from-an-address) * [`POST /inventory-levels/{id}/adjust`](/api-reference/admin/inventory/correct-a-stock-count) * [`POST /offers`](/api-reference/admin/offers/create-an-offer) * [`POST /offers/{id}/codes`](/api-reference/admin/offers/add-a-code-to-an-offer) * [`POST /offers/{id}/codes/bulk`](/api-reference/admin/offers/add-up-to-250-codes-to-an-offer) * [`POST /offers/{id}/codes/{codeId}/disable`](/api-reference/admin/offers/disable-a-code) * [`POST /offers/{id}/codes/{codeId}/enable`](/api-reference/admin/offers/enable-a-disabled-code) * [`POST /offers/{id}/pause`](/api-reference/admin/offers/pause-an-offer) * [`POST /offers/{id}/publish`](/api-reference/admin/offers/publish-an-offer) * [`POST /offers/{id}/resume`](/api-reference/admin/offers/resume-a-paused-offer) * [`POST /orders`](/api-reference/admin/orders/create-an-order) * [`POST /orders/{id}/cancel`](/api-reference/admin/orders/cancel-an-order) * [`POST /orders/{id}/comments`](/api-reference/admin/orders/comment-on-an-order) * [`POST /orders/{id}/confirm`](/api-reference/admin/orders/confirm-an-order) * [`POST /orders/{id}/schedule`](/api-reference/admin/orders/schedule-a-call-back) * [`POST /products`](/api-reference/admin/products/create-a-product) * [`PUT /data/{ownerType}/{ownerId}/{key}`](/api-reference/admin/data/write-a-value) Also the code of a 400 refusal that lists its invalid parameters, when it carries no more specific code. The shape of every problem, and how `@tassyir/app` reports it: [Errors](/guides/errors). # Admin API conventions Source: https://developers.tassyir.io/guides/admin-api How every Admin API route behaves, from versions, money and cursors to idempotent writes, errors and rate limits. The Admin API is the versioned REST API your app calls to read and change a business. Every route follows the conventions on this page, so learn them once: how lists page, how a write is made safe to retry, how errors read and how fast you may call. ## Calling it `https://api.tassyir.io/apps/2026-10/`, with an access token: ```bash theme={null} curl https://api.tassyir.io/apps/2026-10/business \ -H "Authorization: Bearer $TASSYIR_ACCESS_TOKEN" ``` ```json theme={null} { "data": { "id": "741939488313319175", "name": "Dar Amina", "currency": "DZD", "country": "DZ", "timezone": "Africa/Algiers", "createdAt": "2026-01-02T03:04:05.000Z" } } ``` The business comes from the token: no route takes a business id. `GET /business` needs no scope; every other route needs the scope its guide names. With `@tassyir/app`, paths are relative to `/apps/2026-10`, and paths, parameters, bodies and answers are typed: ```ts theme={null} const { data: business } = await admin.get('/business'); const { data: order } = await admin.get('/orders/{id}', { path: { id: '741000000000000001' }, query: { fields: ['orderNumber', 'status', 'total', 'currency'] }, }); console.log(business.timezone, order.status, order.total); ``` | Resource | Guide | | - | - | | `orders`, `cancel-reasons` | [Orders](/guides/orders) | | `products`, `collections`, `images` | [Products and collections](/guides/products-and-collections) | | `inventory-levels` | [Stock](/guides/stock) | | `customers` | [Customers and contact data](/guides/customers) | | `offers` | [Offers](/guides/offers), [Write offers and codes](/guides/write-offers) | | `stores`, `delivery-agents`, `delivery-fee-groups` | [Stores and delivery](/guides/stores-and-delivery) | | `data` | [App data](/guides/app-data) | ## Versions The version is in the path: `2026-10` is the first and, today, the only one. Within a version, fields may be added, never renamed, retyped or removed, so ignore fields you do not know. A version that does not exist answers 404. Versions are released quarterly and supported for 12 months. Tassyir emails you 90 days before a version you use is retired; once it is, its requests are answered by the oldest supported version, with the headers `Tassyir-Api-Version-Served` and `Deprecation`. ## Values * JSON fields are **camelCase**. Ids are **strings**: never parse them as numbers. Times are ISO 8601 in UTC (`2026-10-11T09:12:44.120Z`). * **Money is a decimal string** with two decimals (`"2500.00"`) next to a `currency` field (`DZD` today), never a number. Keep it a string; for arithmetic, use a decimal library or integer cents, never `parseFloat`. An amount Tassyir does not know is `null`, never `"0.00"`. When you send money (a product's price), send a decimal string too. * Open vocabularies (an order's `status`, a carrier's status) are strings, not enums: a merchant or a carrier can add values. * **Contact fields** (a buyer's name, phones, email, address, an order's notes) are present only with `read_customer_contact`. Without it the keys are **absent**, not `null` ([Customers and contact data](/guides/customers)). ## One resource, and lists A single resource is `{ "data": { … } }`. A list is `{ "data": [ … ], "nextCursor": "…" | null }`: | Parameter | Rule | | - | - | | `limit` | 1 to 100, default 50. Anything else is 400 `VALIDATION_FAILED`, never clamped. | | `cursor` | The previous page's `nextCursor`, unchanged. Omit it for the first page. | | `fields` | Trims each item ([below](#fields)). | * Lists are **newest first** and **keyset-paged**: a row added while you walk a list never shifts the pages to come, and no row is repeated or skipped. New rows appear on a new walk from the first page. * `nextCursor` is **opaque**: never build or edit one. It belongs to its list and its filters; with another list or other filters it is 400 `INVALID_CURSOR`. `nextCursor: null` is the last page. * A page can hold fewer than `limit` items while `nextCursor` is set (app data drops values whose owner you can no longer read). Stop on `null`, not on a short page. * **Unknown parameters are refused** (400 `VALIDATION_FAILED`, naming them): a misspelt filter never silently returns everything. `admin.paginate` walks every page for you: ```ts theme={null} let pending = 0; for await (const order of admin.paginate('/orders', { query: { status: 'pending', limit: 100, fields: ['id'] }, })) { pending += 1; } console.log(`${pending} pending orders`); ``` ## Fields `?fields=status,total,buyer` trims each resource to the named **top-level** fields; `id` is always kept. An unknown name is 400 `INVALID_FIELDS`, with the `allowedFields`. Trimming happens after masking: naming a field your installation may not read returns nothing for it. With the SDK, `fields` is a typed list: `fields: ['status', 'total']`. ## Idempotent writes Every write (`POST`, `PUT`, `PATCH`, `DELETE`) accepts an `Idempotency-Key` header: 1 to 255 visible ASCII characters, usually a UUID. Send one, and a retry of the same write never runs it twice. Keys belong to your installation and are kept 24 hours. | Situation | Answer | | - | - | | First use | The write runs. A success is stored for 24 hours; a failure is not, so you can retry. | | Same key, same method, path, query and body | The stored answer and status, with `Idempotent-Replayed: true`. Nothing runs again. | | Same key, anything else different | 422 `IDEMPOTENCY_KEY_REUSED` | | Same key while the first request still runs | 409 `IDEMPOTENCY_KEY_IN_USE` (a key whose request died is freed within 100 seconds) | | A malformed key | 400 `IDEMPOTENCY_KEY_INVALID` | | Tassyir cannot check keys right now | 503 `IDEMPOTENCY_UNAVAILABLE`, with `Retry-After`. The write was **not** attempted. | `@tassyir/app` sends a new key with every write and reuses it for its own retries. Pass your own when a retry may come from elsewhere (a job that can run twice), or `false` to send none: ```ts theme={null} await admin.post('/orders/{id}/confirm', { path: { id: '741000000000000001' }, idempotencyKey: 'confirm-741000000000000001', }); ``` ## Errors Every error under `/apps/2026-10/` is `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)): ```json theme={null} { "type": "https://developers.tassyir.io/errors/permission-denied", "title": "Permission denied", "status": 403, "detail": "You do not have permission: orders:read", "code": "PERMISSION_DENIED", "permission": "orders:read", "fix": "Request the read_orders scope in the app's manifest; it applies once the merchant consents to the new version.", "requestId": "a1b2c3" } ``` * **Act on `code`**, never on `detail` or `title`: `code` is stable. * **`fix`** is one sentence saying what to do, when there is one. * Other members name what was refused: `permission`, `storeId`, `limit`, `errors`, `allowedFields`, … * **`requestId`** (also the `X-Request-Id` header) names the request: quote it when you ask Tassyir for help. * A 5xx never carries an internal message: retry with backoff. `@tassyir/app` turns a problem into a `TassyirApiError`, with `code`, `fix`, `status`, `requestId`, `retryAfterSeconds` and the whole `problem`. The codes each call can answer are typed: ```ts theme={null} import { TassyirApiError, type AdminApiErrorCode } from '@tassyir/app'; type ConfirmError = AdminApiErrorCode<'post', '/orders/{id}/confirm'>; export async function confirm( id: string, ): Promise<'confirmed' | 'waiting' | ConfirmError> { try { await admin.post('/orders/{id}/confirm', { path: { id } }); return 'confirmed'; } catch (error) { if (!(error instanceof TassyirApiError)) throw error; if (error.code === 'ORDER_LIMIT_REACHED') return 'waiting'; // past the plan's allowance console.warn(error.code, error.fix, error.requestId); return error.code as ConfirmError; } } ``` [Errors](/guides/errors) lists every code, with what to do about the common ones. The token endpoint is the one exception: it answers OAuth errors ([Tokens](/guides/tokens#the-token-endpoint-without-the-sdk)). ### Stores: 460, and 404 An installation reaches only the stores its merchant gave it. A store your request names (a `storeId` filter or field) outside them is **HTTP 460 `STORE_SCOPE_FORBIDDEN`**. A resource of another store, named by its id, reads as **404 `NOT_FOUND`**: the API never says it exists. ## Rate limits Every answer carries your installation's bucket: | Header | Meaning | | - | - | | `RateLimit-Limit` | The bucket's size | | `RateLimit-Remaining` | Requests accepted right now | | `RateLimit-Reset` | Seconds until the bucket is full again | * **Your installation's bucket**: by default 40 requests at once, refilled at 4 a second. Past it: **429 `RATE_LIMITED`**, with `Retry-After` (seconds until one more request fits). Online and offline tokens of an installation share one bucket. * **The business's ceiling**: all the apps of one business share, by default, 20 requests at once and 20 a second. Past it: **429 `BUSINESS_RATE_LIMITED`**, with `Retry-After`; then other apps of the business are using the room, so spread your calls out. * **A busy API**: when the API is already running its maximum of app requests, it answers **503 `API_BUSY`** with `Retry-After: 1`, having done **nothing**: any call, a write included, can be retried as it was. * Image uploads have their own daily quota ([Products and collections](/guides/products-and-collections#limits)). The numbers are defaults Tassyir may tune: read the headers rather than hard-coding them. Retries are off by default in `@tassyir/app`; with `retries: 3` it waits `Retry-After` and retries 429 and 503 `API_BUSY` (writes included), any other 503, network errors, 502 and 504 for calls safe to repeat (reads, and writes with a key), and 500 for reads only. ## Plans No plan is needed to install an app or call the API. A plan's limits still apply to what your app does, as they apply to the merchant: the service refuses with the code the dashboard would show (`PLAN_FEATURE_REQUIRED`, `ORDER_LIMIT_REACHED`, `SUBSCRIPTION_REQUIRED`). ## Masking * Without `read_customer_contact`, contact fields are absent. * When the merchant's plan hides phones (no active subscription, or a billing grace that hides them), phones read `**********`. * A **waiting** order (past the plan's order allowance, `waiting: true`) is masked whatever your scopes: phones `**********` and notes `null`, until it unlocks (`orders/updated` with `waiting`). ## Caching Every answer is `Cache-Control: no-store`: it is a merchant's private data. Cache only what your app needs, and drop it on `app/uninstalled`. # App data Source: https://developers.tassyir.io/guides/app-data Keep your app's values in Tassyir, next to the business, its stores, products, orders and customers, under keys your manifest declares. Your app often needs to remember something about a merchant's resource: a product's review rating, an order's sync state with another system, a customer's loyalty points, a setting for the whole business. App data keeps those values in Tassyir, next to the resource, so your app needs no database of its own for them. ## Declare the keys Every key is declared in the manifest: ```toml theme={null} [[data.definitions]] owner = "product" # business | store | product | order | customer key = "rating" # [a-z][a-z0-9_]{0,63}, unique per owner type = "json" # string | number | integer | boolean | json visibility = "app" # app | merchant | storefront ``` * **The installation's version decides.** A key your new version adds can be written on an installation only once its merchant accepts that version. * **The type is checked on every write.** The visibility comes from the declaration, never from the request. * `visibility`: `app` values are read by your app only. `merchant` values are meant for the dashboard and `storefront` values for the store's pages; neither is shown there yet, so declare them only for values you would be happy to see shown. * **A key a newer version drops** keeps its values: still readable (`declared: false`) and deletable, never writable. A key whose type changes keeps its old values; the next write must match the new type. ## Owners and scopes Reading or writing a value needs the scope that reads its owner. Writing a value changes nothing about the owner, so no `write_*` scope is needed. | Owner | Scope | Owner id | | - | - | - | | `business` | none | the installation's business | | `store` | `read_stores` | a store | | `product` | `read_products` | a product | | `order` | `read_orders` | an order | | `customer` | `read_customers` | a customer | The owner is read first, as `GET /products/{id}` (or the others) would read it: an owner your token cannot read is 404 `NOT_FOUND`, a store outside your installation's is 460. So an online token never touches a value about something its person cannot see. ## Write, read, delete ```ts theme={null} const owner = { ownerType: 'product', ownerId: '741000000000000500', key: 'rating', } as const; // PUT writes the whole value: 201 when it is new, 200 when it replaced one. await admin.put('/data/{ownerType}/{ownerId}/{key}', { path: owner, body: { value: { average: 4.5, count: 12 } }, }); const { data: rating } = await admin.get('/data/{ownerType}/{ownerId}/{key}', { path: owner, }); console.log(rating.value, rating.visibility, rating.updatedAt); await admin.delete('/data/{ownerType}/{ownerId}/{key}', { path: owner }); ``` ```json theme={null} { "id": "9500000000000000001", "ownerType": "product", "ownerId": "741000000000000500", "key": "rating", "value": { "average": 4.5, "count": 12 }, "visibility": "app", "declared": true, "createdAt": "2026-10-14T08:00:00.000Z", "updatedAt": "2026-10-14T09:30:00.000Z" } ``` * The body is `{ "value": … }` and nothing else. A value is never `null`: remove one with `DELETE`. * A missing value answers 404 `NOT_FOUND` (its `detail` says whether the owner or the value is missing). * `GET /data` lists your installation's values, newest first. Filters: `ownerType`, `ownerId` (with `ownerType`), `key`. Values whose owner you can no longer read are left out, so **a page can be shorter than `limit` while `nextCursor` is set**: keep reading until it is `null`. ```ts theme={null} for await (const value of admin.paginate('/data', { query: { ownerType: 'product', key: 'rating' }, })) { console.log(value.ownerId, value.value); } ``` ## Values | Type | Accepted | | - | - | | `string` | a JSON string | | `number` | a finite JSON number | | `integer` | a safe integer, -(2^53 - 1) to 2^53 - 1 | | `boolean` | `true` or `false` | | `json` | any JSON value: an object, array, string, number or boolean | * At most **16,384 bytes** of compact JSON in UTF-8 (an Arabic letter is two bytes): 400 `APP_DATA_VALUE_TOO_LARGE`. * Refused with 400 `APP_DATA_VALUE_INVALID` and a `reason`: `wrong_type`, `not_finite`, `nul_character`, `lone_surrogate`, `reserved_key` (an object key `__proto__`, `constructor` or `prototype`), `too_deep` (more than 32 levels). * An object's keys come back in the order Tassyir stores them (shorter keys first), not the order you sent. * A key the version does not declare for that owner is 400 `APP_DATA_KEY_NOT_DECLARED`. ## Limits An installation holds at most **50,000 values**, whatever their owners. Writing a new key past that is 409 `APP_DATA_LIMIT_REACHED` (`limit`, `used`); rewriting a value that exists is never refused, and `DELETE` frees a slot. Values of keys a version dropped count until you delete them. ## When values go away * Values stay when their owner is deleted, until you delete them; they drop out of lists and reads. * When the merchant uninstalls your app, its values are deleted 48 hours later, and your app receives `business/redact`. A reinstall, even within those 48 hours, is a new installation that starts empty, and an uninstalled installation can no longer read its values: if a value must survive a reinstall, keep your own copy ([Compliance and uninstall](/guides/compliance-and-uninstall)). # Compliance and uninstall Source: https://developers.tassyir.io/guides/compliance-and-uninstall What happens when a merchant uninstalls your app, and how to honour buyer data requests and the deletion that follows an uninstall. A merchant can uninstall your app at any time, and buyers can ask for their data to be exported or erased. Tassyir tells your app through webhooks, and your app must do its part: stop using the business's data, and delete or export what it holds. ## Subscribe to the compliance topics ```toml theme={null} [[webhooks]] topics = ["app/uninstalled", "customers/data_request", "customers/redact", "business/redact"] uri = "/webhooks" ``` The compliance topics need no scope. A custom app is not required to subscribe to them; an app in the marketplace is (the review checks it: [Publishing to the marketplace](/guides/publishing#the-review-checklist)). Subscribe anyway if your app keeps any data about a business or its buyers: Tassyir deletes the data it holds for your app either way, but only your app can delete what your servers hold. ## Uninstall When a merchant uninstalls your app: 1. **Every token is revoked at once.** The next Admin API call is 401 `APP_TOKEN_INVALID`, and a refresh is refused. 2. **`app/uninstalled`** is sent to that installation (`data.installation`, with `status: "uninstalled"` and `uninstalledAt`). Forget its tokens, stop its jobs, and stop calling the API for it. 3. **48 hours later**, Tassyir deletes every [app data](/guides/app-data) value the installation kept, and sends **`business/redact`** to that installation (`data.business.id`). Delete everything your servers still hold about the business for it. On [Tassyir Hosting](/guides/tassyir-hosting#webhooks), the installation's own database (`ctx.props.STORAGE`) is deleted with its app data, and both deliveries reach your code with `ctx.props.TASSYIR` refused (`HOSTED_CALL_REFUSED`). ```ts theme={null} import type { VerifiedWebhook } from '@tassyir/app'; // Called with a verified delivery (see Webhooks). export async function onLifecycle(delivery: VerifiedWebhook): Promise { switch (delivery.event.type) { case 'app/uninstalled': await tassyir.tokens.forget(delivery.installationId); // Stop scheduled work for this installation. break; case 'business/redact': // Delete what you keep for this installation of delivery.businessId. break; } } ``` [Webhooks](/guides/webhooks#verify-every-delivery) shows how to verify a delivery. * A **reinstall** is a new installation, with a new id, a new consent and no data, even within the 48 hours. The old installation's `business/redact` still comes, naming the old installation id: delete by installation, not by business. * `business/redact` reaches only the uninstalled installation it names, and is redelivered like any webhook: answer 200 when you already handled it, even though the installation is gone. ## Buyer data requests `customers/data_request` and `customers/redact` are sent when Tassyir exports or erases a buyer's data for a business. ```json theme={null} { "id": "evt_741000000000000002", "type": "customers/redact", "apiVersion": "2026-10", "createdAt": "2026-10-12T10:00:00.000Z", "businessId": "632000000000000001", "storeId": null, "appId": "741000000000000900", "installationId": "755000000000000001", "changed": [], "data": { "customer": { "id": "741000000000001000", "phone": "+213555123456" }, "orderIds": ["741000000000000001", "741000000000000002"] } } ``` * `data.customer.id` is the customer ([Customers](/guides/customers)); `data.customer.phone` is there only with `read_customer_contact`, and reads `**********` when the plan hides phones or when every order it names was waiting. `data.orderIds` are the buyer's orders your installation could see. * An installation limited to selected stores is told only about orders in its stores, and gets no delivery when none is left: it never saw this buyer. * **`customers/redact`**: delete or anonymise everything your app holds about that buyer and those orders. Tassyir has already erased the buyer from the orders; the orders themselves stay, as the merchant's records. * **`customers/data_request`**: gather what your app holds about that buyer and make it available to the merchant. These bodies are built from what Tassyir recorded when the request was made, so a replay within 7 days carries the same buyer even though the data is gone. ## Suspension Tassyir can suspend an installation, or a whole app. Its tokens are revoked, its page cannot be opened, and its webhooks are skipped until it is resumed. ## A checklist * Subscribe to `app/uninstalled`, `customers/data_request`, `customers/redact` and `business/redact`. * On `app/uninstalled`: forget the tokens and stop the installation's work. * On `customers/redact`: erase the buyer from your systems. * On `customers/data_request`: be able to tell the merchant what you hold. * On `business/redact`: delete what you hold for that installation. * Deduplicate on `webhook-id`, and answer 2xx to a delivery you already handled. # Customers and contact data Source: https://developers.tassyir.io/guides/customers Read buyers and their delivery record, and handle buyers' names, phones and addresses with the read_customer_contact scope. A customer is a buyer of the business, known by phone number and built from their orders: they have no account. An app reads customers to see how each buyer's orders went (a review app skips buyers who returned parcels, a loyalty app rewards repeat buyers). Buyers' names, phones and addresses are the most sensitive data a merchant holds, and they have a scope of their own. | Route | Scope | | - | - | | `GET /customers`, `GET /customers/{id}` | `read_customers` (it implies `read_orders`) | | A buyer's name, phone and address | `read_customer_contact` | ## The customer ```json theme={null} { "id": "741000000000001000", "provinceCode": 16, "townOns": 1601, "name": "Amina B.", "phone": "0555123456", "ordersCount": 5, "openCount": 1, "deliveredCount": 3, "returnedCount": 1, "canceledCount": 0, "currency": "DZD", "deliveredValue": "11700.75", "firstOrderAt": "2026-06-01T10:00:00.000Z", "lastOrderAt": "2026-10-11T07:00:00.000Z", "blocked": false, "createdAt": "2026-09-26T00:00:00.000Z", "updatedAt": "2026-10-11T07:00:20.000Z" } ``` * **The delivery record is always there:** orders placed (exchanges, waiting, archived and abandoned ones left out), open, delivered, returned and cancelled; the value of delivered orders (`deliveredValue`, a decimal string); the first and last order; whether the merchant blocked the buyer. * `provinceCode` and `townOns` (the wilaya and commune of their newest order that has one) are not contact data. * **`name` and `phone` need `read_customer_contact`.** A buyer's second phone and address are on their orders (`buyer.phone2`, `buyer.address`), under the same scope. * The merchant's private note about a buyer is never sent. * A customer can be about 20 seconds behind their latest order. `GET /customers` lists every buyer with at least one counted order, newest first. `storeId` lists the buyers of one store, **with that store's counters**. An installation limited to selected stores sees a buyer only through orders in its stores, and reads those stores' counters summed, never the whole business's. Search and segments are not offered: page the list, and follow `customers/updated`. ## `read_customer_contact` Without it, contact fields are **left out**: the keys are absent, not `null`, so your app can tell "not granted" from "empty". It covers: * an order's or checkout's `buyer.name`, `phone`, `phone2`, `email`, `address`, `address2`, and the order's `note` and `deliveryNote` (buyers type contact details into notes); * another author's order comments; * a customer's `name` and `phone`; * the same fields in webhook bodies, and the customer picker's `name` and `phone` ([Embedded pages](/guides/embedded-pages#pickers)). The scope is marked **sensitive** on the install screen, every Admin API answer that carries contact fields is logged, and only a member who sees every order can grant it. It needs `read_orders`. Even with it: * **when the merchant's plan hides phones** (no active subscription, or a billing grace that hides them), every phone reads `**********`; * **a waiting order** (past the plan's order allowance) shows `**********` for its phones and `null` for its notes and other authors' comments, until it unlocks. ```ts theme={null} const { data: customer } = await admin.get('/customers/{id}', { path: { id: '741000000000001000' }, }); if (customer.phone === undefined) { console.log('no read_customer_contact: ask for it in a new version'); } else if (/^\*+$/.test(customer.phone)) { console.log('the phone is hidden right now'); } ``` ## Handle buyer data with care * Ask for `read_customer_contact` only if your app does something with the buyer's contact (sending a message, printing a label). Make it optional when only some merchants will want that feature. * Store as little as you can, for as short as you can, and never in logs. * Honour `customers/redact` and `customers/data_request`, and delete what you hold about a business on `business/redact` ([Compliance and uninstall](/guides/compliance-and-uninstall)). # Dev stores Source: https://developers.tassyir.io/guides/dev-stores Build and test on businesses seeded with synthetic data, where nothing reaches a carrier, a buyer or a payment provider. A dev store is a business you create from the developer console to build and test apps on. It looks and works like a merchant's business, in the dashboard and through the Admin API, with synthetic data. Nothing it does reaches an outside party or costs money, and it needs no subscription. You own it, so your custom apps install there like on any business you own. ## Create one In the developer console, open **Dev stores** and create one, with a name and seed data (the default) or empty. | Limit | Value | | - | - | | Dev stores per developer account | 3, and they cannot be deleted | | New orders a day (dashboard and API together) | 50, per Algiers day | | Stores in a dev store | 3 | | People in a dev store | 5 (invitations are refused) | ## The seed data * **30 products**, most with options (size, colour, shoe size, volume) and a variant per combination. The first 8 track stock (20 to 80 units each). No images and no collections. * **64 buyers**, every one with a phone in the reserved block `+213 500 000 000` to `+213 500 000 999`, and no email. Together they cover all 58 wilayas. * **200 orders** over the last 60 days, in every state: pending, incomplete, confirmed, scheduled, cancelled, dispatched, delivered, returned, and abandoned checkouts. Shipped orders ride a private courier. Seeding sends no webhook: the data exists before your app is installed. To see webhooks, change something after installing (create or confirm an order, edit a product). ## What is switched off In the dashboard, an action that would reach an outside party is refused with **409 `DEV_STORE_RESTRICTED`**, naming the `action` it stopped and a `fix`: | `action` | What it stops | | - | - | | `carrier` | Linking a carrier account, and dispatching to a carrier (use a private courier) | | `integration` | Connecting other platforms, spreadsheets, ad accounts and conversion tracking | | `whatsapp` | Connecting a WhatsApp number | | `email` | Team invitations | | `billing` | Checkout, plan changes, refunds and credit purchases | | `ai` | AI images and pages, product imports and scraping | | `custom_domain` | Adding a custom domain | | `storefront_checkout` | Orders and checkouts from the storefront | | `cargo` | Tassyir Cargo | Through the Admin API, the one refusal particular to dev stores is the daily order cap: `POST /orders` answers **429 `DEV_STORE_ORDER_LIMIT_REACHED`**, with `retryAfterSeconds` (until the next Algiers day). The storefront says it is a development store, takes no orders, is hidden from search engines and carries no tracking pixel. **Still on:** webhooks to your app (that is the point), image uploads (`POST /images`, within a tenth of the usual daily quota), and push notifications to your own devices. ## The plan A dev store has every plan feature, with the limits above. It is never billed, has no order allowance (no order is ever `waiting`) and its phones are never hidden for want of a subscription. To test what an app does on a smaller plan, or with waiting orders and hidden phones, use a real business on that plan. # Embedded pages and App Bridge Source: https://developers.tassyir.io/guides/embedded-pages Show your app's page inside the Tassyir dashboard and use App Bridge for ID tokens, toasts, modals, the save bar, navigation and pickers. Your app can have a page inside the Tassyir dashboard, next to the merchant's orders and products. The dashboard shows it in a frame and talks to it through **App Bridge**: your page asks for ID tokens, shows toasts and modals, uses the dashboard's save bar and title bar, navigates, and opens the dashboard's pickers. `@tassyir/app-bridge` is the page's side of it. ## How the page is loaded Set `admin.page` in the manifest (usually `/`). When the merchant opens your app, the dashboard address is `https://app.tassyir.io/{locale}/dashboard/{businessId}/apps/{handle}/{path}`, and your frame loads: > `hosting.url` + `admin.page` + the app path (`{path}`) + the dashboard's own > query string So every path below `admin.page` is your page: serve the same document for all of them and route on the client. * Allow the dashboard to frame your page, and nothing else: `Content-Security-Policy: frame-ancestors https://app.tassyir.io`. * The page counts as shown at its frame's `load`, or at its first App Bridge call. With neither within 15 seconds, the dashboard shows a "didn't load" state with **Try again**. * **Tassyir vouches for nothing in your page's address.** Never read a business, a user or a store from it to decide what to show or allow. App Bridge's context is for display only. Who is looking is what a verified ID token says ([Tokens](/guides/tokens#who-is-looking-the-id-token)). * The dashboard never gives your page a Tassyir credential: the ID token is the only one. Your frame's cookies are third-party cookies, which browsers often block: do not build a session on them. ## A page ```ts browser theme={null} import { createAppBridge, isAppBridgeError } from '@tassyir/app-bridge'; // Create it once, as early as the page loads: its first message tells the // dashboard the page is up. const bridge = createAppBridge(); // Each request to your server carries a NEW ID token: never keep one. async function api(path: string, init: RequestInit = {}): Promise { const { token } = await bridge.idToken(); const headers = new Headers(init.headers); headers.set('Authorization', `Bearer ${token}`); const response = await fetch(path, { ...init, headers }); if (!response.ok) throw new Error(`${path} answered ${response.status}`); return (await response.json()) as T; } // The frame cannot see the dashboard's language, direction or theme. bridge.on('context.changed', ({ locale, dir, theme }) => { document.documentElement.lang = locale; document.documentElement.dir = dir; document.documentElement.dataset['theme'] = theme; }); await bridge.titleBar.set({ title: 'Reviews' }); const reviews = await api<{ id: string; orderNumber: string }[]>('/api/reviews'); document.querySelector('#count')!.textContent = String(reviews.length); document.querySelector('#pick')!.addEventListener('click', async () => { try { const { selection } = await bridge.resource.pick({ type: 'product', multiple: true, }); if (selection.length === 0) return; // the merchant cancelled await api('/api/featured', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(selection.map((product) => product.id)), }); await bridge.toast.show({ message: `${selection.length} products featured`, tone: 'success', }); } catch (error) { if (!isAppBridgeError(error)) throw error; // error.code is stable; error.fix says what to do about it. await bridge.toast.show({ message: error.message, tone: 'error' }); } }); ``` Your server verifies each token with `tassyir.authenticate(idToken)` before it answers ([Tokens](/guides/tokens#embedded-apps-one-offline-token-per-installation)). ## Calls Every call posts one message to the dashboard and resolves with its answer, or rejects with an `AppBridgeError`. The SDK checks each payload before posting, so most mistakes are refused without a round trip. | Call | Does | | - | - | | `idToken()` | A new 60-second ID token for this installation and the person looking. Never cached. | | `toast.show({ message, tone? })` | A toast signed with your app's name. `tone`: `info` (default), `success`, `error`. | | `modal.open({ path, title?, size? })` | One of your pages in a dashboard modal; resolves `{ closedBy: 'app' \| 'merchant' }` when it closes. | | `modal.close()` | Closes the modal, from the page or from the modal itself. | | `navigate({ path, replace? })` | Says your app moved to another of its pages; the dashboard's address follows. | | `navigate({ dashboard, id? })` | Leaves your app for a dashboard page. | | `saveBar.show({ saving? })` | Shows the dashboard's save bar, busy while `saving` is true. | | `saveBar.hide()` | Hides it. | | `titleBar.set({ title })` | Your page's title beside your app's name; `null` or `""` clears it. | | `resource.pick({ type, multiple? })` | The dashboard's picker for products, collections or customers. | | `context.get()` | The context: locale, direction, theme, current store. | * **Text** (toasts, modal and page titles) is shown as text: control and bidi characters become spaces, white space is folded, and it is cut (150 characters for a toast, 80 for a title). More than 2,000 characters is refused. * **App paths** (`navigate`, `modal.open`) are relative to `admin.page` and start with one `/`, with an optional query: `/reviews/2?tab=sent`. No scheme or host, backslash, white space, control or bidi character, fragment, or `.`, `..` or empty segment. At most 1,024 characters before the `?` and 2,048 in all. * **`navigate({ path })`** does not reload your frame: show the page yourself (with your router), and the dashboard's address follows, as a new history entry or, with `replace: true`, in place. Back and Forward reload your frame at the address they land on. * **`navigate({ dashboard })`** opens `home`, `orders`, `order` (with `id`), `customers`, `customer` (with `id`), `tracking`, `inventory`, `products` (the current store's) or `apps`. * **A modal's page** may call only `idToken`, `toast.show`, `modal.close`, `resource.pick` and `context.get`. One modal and one picker at a time. * `modal.open` and `resource.pick` wait for the merchant as long as the dialog is open. Every other call times out after 10 seconds, and every call takes an `AbortSignal`. ## The save bar Show the dashboard's save bar while your page has unsaved changes. Its buttons send events to your page: ```ts browser theme={null} import { createAppBridge } from '@tassyir/app-bridge'; const bridge = createAppBridge(); let draft = { highlightFrom: '5000.00' }; document.querySelector('form')!.addEventListener('input', () => { void bridge.saveBar.show(); }); bridge.on('saveBar.save', async () => { await bridge.saveBar.show({ saving: true }); const { token } = await bridge.idToken(); const response = await fetch('/api/settings', { method: 'PUT', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', }, body: JSON.stringify(draft), }); if (!response.ok) { // Keep the draft and the bar, so the merchant can save again. await bridge.saveBar.show({ saving: false }); await bridge.toast.show({ message: 'Not saved, try again', tone: 'error' }); return; } await bridge.saveBar.hide(); await bridge.toast.show({ message: 'Saved', tone: 'success' }); }); bridge.on('saveBar.discard', async () => { draft = { highlightFrom: '5000.00' }; await bridge.saveBar.hide(); }); ``` The bar does not stop the merchant from leaving the page. When the page goes away, the SDK hides the bar and clears the title, so neither outlives it. ## Pickers `resource.pick` resolves when the merchant closes the dashboard's picker: `selection` is empty when they cancelled, holds one item unless `multiple` is true, and at most 50. | `type` | Each item | Your installation needs | | - | - | - | | `product` | `{ id, name, image }` (`image` an https URL or `null`) | `read_products` | | `collection` | `{ id, name }`, from the current store | `read_products` | | `customer` | `{ id, provinceCode }`, plus `name` and `phone` with `read_customer_contact` | `read_customers` | The person looking must also be allowed to read what the picker shows. An installation limited to selected stores picks from the current store only, which must be one of its stores. ## Context ```ts browser theme={null} const stop = bridge.on('context.changed', ({ locale, dir, theme, storeId }) => { console.log(locale, dir, theme, storeId); }); console.log(bridge.context.current); // null until the dashboard has sent it stop(); ``` The context is `{ locale: 'ar' | 'en', dir: 'rtl' | 'ltr', theme: 'light' | 'dark', storeId }`. `storeId` is the dashboard's current store, or `null` when there is none or your installation is limited to other stores. Follow `locale`, `dir` and `theme` so your page reads like the dashboard: Arabic pages run right to left. The context is for display; it proves nothing. ## Errors An `AppBridgeError` has a stable `code`, the `call` that failed, a `source` (`dashboard` or `sdk`), a `message` for people and a `fix`. `isAppBridgeError(error, code?)` narrows an unknown error. | Code | Meaning | | - | - | | `INVALID_PAYLOAD` | The payload breaks the call's rules. | | `RATE_LIMITED` | Over the call's limit for the minute. | | `ID_TOKEN_UNAVAILABLE` | Tassyir cannot sign ID tokens right now; the dashboard tells the merchant. | | `APP_UNAVAILABLE` | The installation cannot be opened any more (uninstalled, suspended, or no page). | | `PERMISSION_DENIED` | The person looking may not do this. | | `SCOPE_REQUIRED` | Your installation lacks the scope a pick needs. | | `STORE_NOT_ALLOWED` | Your installation is limited to other stores than the current one. | | `NO_STORE` | A collection pick with no current store. | | `BUSY` | A modal or a picker is already open. | | `NO_MODAL` | `modal.close` with no modal open. | | `NOT_ALLOWED_HERE` | A call a modal's page may not make. | | `INVALID_DESTINATION` | A place the dashboard cannot open now. | | `FAILED` | Anything else; try again. | | `NOT_EMBEDDED` | The page is not inside the dashboard's frame (`source: 'sdk'`). | | `TIMEOUT` | No answer in time (`source: 'sdk'`). | The [`@tassyir/app-bridge` README](https://github.com/tassyir/apps-sdk/tree/main/packages/app-bridge#errors) lists the SDK's other codes. ## Limits The dashboard counts calls per installation over a sliding minute, and answers `RATE_LIMITED` past a call's limit. Past 240 messages a minute in all, it drops messages without answering. The SDK counts what your page sends and refuses first, so your page sees `RATE_LIMITED` rather than a timeout. | Call | Per minute | | - | - | | `idToken` | 30 | | `toast.show` | 20 | | `modal.open` | 10 | | `modal.close` | 30 | | `navigate` | 30 | | `saveBar.show` | 60 | | `saveBar.hide` | 60 | | `titleBar.set` | 60 | | `resource.pick` | 10 | | `context.get` | 60 | A reloaded frame does not reset them: they last as long as the dashboard tab. # Errors Source: https://developers.tassyir.io/guides/errors Every error code the Admin API and the token endpoint answer, and what to do about the common ones. Every refusal says what failed and how to fix it. Act on the stable `code`, show or log the `fix`, and quote the `requestId` when you ask Tassyir for help. ## The shape The Admin API answers errors as `application/problem+json`: ```json theme={null} { "type": "https://developers.tassyir.io/errors/permission-denied", "title": "Permission denied", "status": 403, "detail": "You do not have permission: orders:read", "code": "PERMISSION_DENIED", "permission": "orders:read", "fix": "Request the read_orders scope in the app's manifest; it applies once the merchant consents to the new version.", "requestId": "a1b2c3" } ``` | Member | Meaning | | - | - | | `code` | Stable and machine-readable. Act on it, never on `title` or `detail`. | | `fix` | One sentence saying what to do, when there is one. | | `status` | The HTTP status, also the answer's. | | `requestId` | Names the request (also the `X-Request-Id` header). | | `type` | `https://developers.tassyir.io/errors/`: an identifier. | | others | What was refused: `permission`, `storeId`, `limit`, `retryAfter`, `errors`, `allowedFields`, … | A 5xx never carries an internal message. The token endpoint is the one exception to this shape: it answers OAuth errors ([below](#token-endpoint-errors)). In `@tassyir/app`, an Admin API error is a `TassyirApiError` (`code`, `fix`, `status`, `requestId`, `retryAfterSeconds`, `problem`), a token endpoint error a `TassyirOAuthError` (its `code` is `OAUTH_` and the OAuth error in capitals), and every other refusal a `TassyirError` with a `code` and a `fix`. App Bridge's errors are listed in [Embedded pages](/guides/embedded-pages#errors), the manifest's in [The manifest](/guides/manifest#validate-before-you-upload), and those of the hosted helpers (`HOSTED_PROPS_MISSING`, and `KEYS_UNAVAILABLE`, `KEYS_NOT_CONFIGURED` and `WEBHOOK_NOT_A_DELIVERY` from `verifyHostedWebhook`) in [Tassyir Hosting](/guides/tassyir-hosting#webhooks). ## Common errors and what to do ### `VALIDATION_FAILED` HTTP 400. A parameter or a field failed validation, or is not one the route takes. `errors` lists each problem. Fix the request: unknown parameters are refused on purpose, so a misspelt filter never silently returns everything. ### `INVALID_CURSOR` HTTP 400. The `cursor` is not this list's, was edited, or was made with other filters. Send `nextCursor` back unchanged, with the same filters; to change a filter, start again without a cursor. ### `INVALID_FIELDS` HTTP 400. `fields` names a field the resource does not have. `allowedFields` lists the valid names; fields are top-level only. ### `APP_TOKEN_REQUIRED` HTTP 401. No `Authorization: Bearer` header. Send the installation's access token. ### `APP_TOKEN_INVALID` HTTP 401. The token is unknown, expired or revoked, or its installation or app is no longer active. Refresh it once (the SDK does). If the refresh is refused, the installation was uninstalled, suspended or had its scopes changed: an embedded app gets a new token on the next page view, a headless app needs a new install code. ### `HOSTED_CALL_REFUSED` HTTP 401. A hosted app's call through `ctx.props.TASSYIR` was refused: its installation is uninstalled or suspended, or no longer runs a released version. It is also the answer during an `app/uninstalled` or `business/redact` delivery. Make the call from the request that received `ctx.props.TASSYIR`; if the installation is active, retry after a short wait ([Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api)). ### `PERMISSION_DENIED` HTTP 403. Your installation lacks the permission the route checks (`permission`). The `fix` names the scope: add it to the manifest, upload a version, and the merchant must accept the update. With an online token, the person may lack it even when your app has it. ### `STORE_SCOPE_FORBIDDEN` HTTP 460. Your request named a store (`storeId`) outside the stores the merchant gave your installation. Use one of its stores; list them with `GET /stores` (`read_stores`). ### `NOT_FOUND` HTTP 404. No such route, or no resource your token can read under that id: a resource of another store or business, or a malformed id, reads the same. ### `RATE_LIMITED` HTTP 429. Your installation's bucket is empty. Wait `Retry-After` seconds, then retry; spread your calls using the `RateLimit-*` headers. ### `BUSINESS_RATE_LIMITED` HTTP 429. All the apps of this business together are past its ceiling. Wait `Retry-After` seconds; other apps are using the room, so slow down. ### `API_BUSY` HTTP 503. The API was too busy to take your call and did **nothing**. Wait `Retry-After` (1 second) and send the same call again, a write included. ### `IDEMPOTENCY_KEY_REUSED` HTTP 422. You reused an `Idempotency-Key` with another method, path, query or body. Use a new key for a different write. ### `IDEMPOTENCY_KEY_IN_USE` HTTP 409. A request with the same key is still running. Wait, then retry with the same key: you will get its answer. ### `IDEMPOTENCY_UNAVAILABLE` HTTP 503. Tassyir cannot check idempotency keys right now, so the write was not attempted. Retry after `Retry-After` with the same key. ### `PLAN_FEATURE_REQUIRED` HTTP 403. The merchant's plan lacks the feature (`feature`), such as stock at a chosen location. Do without it, or tell the merchant. ### `SUBSCRIPTION_REQUIRED` HTTP 422. The route needs a plan, and the business has no usable subscription. Tell the merchant. ### `ORDER_LIMIT_REACHED` HTTP 403. The order is waiting: it is past the plan's order allowance, and cannot be confirmed until it unlocks (`orders/updated` with `waiting`). ### `ORDER_NOT_EDITABLE` HTTP 409. The order is dispatched, delivered or returned (`orderStatus`): it can no longer be edited, confirmed or cancelled. ### `ORDER_STATUS_CONFLICT` HTTP 409. The order's status does not allow this move (`orderStatus`, `requestedStatus`): read the order again and decide. ### `OFFER_NOT_OWNED` HTTP 403. You tried to change, or read the codes or redemptions of, an offer your installation did not create: the merchant's own, or another app's. An app writes only its own offers. Create your own offer with `POST /offers` and change that one ([Write offers and codes](/guides/write-offers#you-write-only-the-offers-you-own)). The merchant's dashboard has a counterpart you never get: editing or duplicating an **app-owned** offer there answers 409 `OFFER_APP_OWNED`, because such an offer is changed in the app that owns it. ### `MEDIA_URL_NOT_ALLOWED` HTTP 400. An image is not on `https://cdn.tassyir.io/` and not already on the resource (`field`, `url`). Upload it with `POST /images` first ([Products and collections](/guides/products-and-collections#bring-an-image-onto-the-cdn)). ### `APP_DATA_KEY_NOT_DECLARED` HTTP 400. Your installation's version does not declare that key for that owner type. Declare it under `[[data.definitions]]`, upload, and have the merchant accept the update. ### `INTERNAL_ERROR` HTTP 500. Tassyir's fault. Retry reads with backoff; retry writes only with the same `Idempotency-Key`. Quote the `requestId` if it persists. ### `SERVICE_UNAVAILABLE` HTTP 503. Something Tassyir depends on is down. Retry with backoff, writes with the same `Idempotency-Key`. ## Hosted apps: the binding's own codes A hosted app calls the Admin API through `ctx.props.TASSYIR.fetch` ([Tassyir Hosting](/guides/tassyir-hosting#call-the-admin-api)). The binding checks each call before it sends it and answers some refusals itself, in the same problem shape but without a `requestId`: they never reached the API. `@tassyir/app` lists all nine in `HOSTED_BINDING_ERRORS`, and `hostedAdmin` throws them as `TassyirApiError`s. Six of them are the API's own codes (`METHOD_NOT_ALLOWED`, `BAD_REQUEST`, `IDEMPOTENCY_KEY_INVALID`, `PAYLOAD_TOO_LARGE`, `UNSUPPORTED_MEDIA_TYPE`, and `SERVICE_UNAVAILABLE` when the API did not answer within 30 seconds). Three exist only there, so the API's reference does not list them: ### `HOSTED_PATH_NOT_ALLOWED` HTTP 400. The call was not to an Admin API path. Call `/apps//…` with segments of letters, digits, `_` and `-` only: no other path, no `//`, no trailing `/`, no percent-encoding and no `;`. The binding also reads Tassyir's two public key documents, with `GET` at exactly `/apps/webhook-keys` and `/apps/.well-known/jwks.json` (another method there is `METHOD_NOT_ALLOWED`). ### `RESPONSE_TOO_LARGE` HTTP 502. The API's answer was larger than a hosted app may receive (8 MiB). Ask for less: a smaller `limit`, or `?fields=` with only the fields you read. ### `TASSYIR_BINDING_NOT_AVAILABLE` HTTP 503. Calls to the Admin API from a hosted app are not available right now; nothing was sent. Try again later. ## Every Admin API code Generated from the OpenAPI document of API version `2026-10` that `@tassyir/app` is typed from (55 operations under `/apps/2026-10/`). An operation lists every code it can answer, per status, in that document; a status fallback (the code of a refusal that carries no more specific one) is in its error catalogue and may come from any operation. | Code | HTTP | Answered by | | - | - | - | | [`API_BUSY`](/errors/api-busy) | 503 | every operation | | [`APP_DATA_KEY_NOT_DECLARED`](/errors/app-data-key-not-declared) | 400 | `PUT /data/{ownerType}/{ownerId}/{key}` | | [`APP_DATA_LIMIT_REACHED`](/errors/app-data-limit-reached) | 409 | `PUT /data/{ownerType}/{ownerId}/{key}` | | [`APP_DATA_VALUE_INVALID`](/errors/app-data-value-invalid) | 400 | `PUT /data/{ownerType}/{ownerId}/{key}` | | [`APP_DATA_VALUE_TOO_LARGE`](/errors/app-data-value-too-large) | 400 | `PUT /data/{ownerType}/{ownerId}/{key}` | | [`APP_TOKEN_INVALID`](/errors/app-token-invalid) | 401 | every operation | | [`APP_TOKEN_REQUIRED`](/errors/app-token-required) | 401 | every operation | | [`BAD_REQUEST`](/errors/bad-request) | 400 | 23 operations | | [`BRAND_NOT_FOUND`](/errors/brand-not-found) | 400 | `PATCH /products/{id}`, `POST /products` | | [`BULK_EXPORTS_NOT_CONFIGURED`](/errors/bulk-exports-not-configured) | 503 | `POST /bulk/exports` | | [`BULK_EXPORT_IN_PROGRESS`](/errors/bulk-export-in-progress) | 409 | `POST /bulk/exports` | | [`BULK_EXPORT_RATE_LIMITED`](/errors/bulk-export-rate-limited) | 429 | `POST /bulk/exports` | | [`BUSINESS_RATE_LIMITED`](/errors/business-rate-limited) | 429 | every operation | | [`CANCEL_NOTE_REQUIRED`](/errors/cancel-note-required) | 400 | `POST /orders/{id}/cancel` | | [`CANCEL_REASON_INVALID`](/errors/cancel-reason-invalid) | 400 | `POST /orders/{id}/cancel` | | [`COLLECTION_CONDITIONAL_LIMIT`](/errors/collection-conditional-limit) | 409 | `PATCH /collections/{id}`, `POST /collections` | | [`COLLECTION_DEPTH_LIMIT`](/errors/collection-depth-limit) | 409 | `PATCH /collections/{id}`, `POST /collections` | | [`COLLECTION_NOT_FOUND`](/errors/collection-not-found) | 400 | `POST /products` | | [`COLLECTION_PARENT_CYCLE`](/errors/collection-parent-cycle) | 409 | `PATCH /collections/{id}` | | [`COLLECTION_PARENT_NOT_FOUND`](/errors/collection-parent-not-found) | 404 | `PATCH /collections/{id}`, `POST /collections` | | [`COLLECTION_PRODUCT_WRONG_STORE`](/errors/collection-product-wrong-store) | 400 | `PATCH /collections/{id}/products` | | [`COLLECTION_RULE_INVALID`](/errors/collection-rule-invalid) | 400 | `PATCH /collections/{id}`, `POST /collections` | | [`CONFLICT`](/errors/conflict) | 409 | any operation, as a fallback | | [`DELIVERY_SPEED_UNAVAILABLE`](/errors/delivery-speed-unavailable) | 400 | `PATCH /orders/{id}`, `POST /orders` | | [`DELIVERY_SPEED_UNSUPPORTED`](/errors/delivery-speed-unsupported) | 400 | `PATCH /orders/{id}` | | [`DEV_STORE_ORDER_LIMIT_REACHED`](/errors/dev-store-order-limit-reached) | 429 | `POST /orders` | | [`FORBIDDEN`](/errors/forbidden) | 403 | `GET /inventory-levels`, `GET /inventory-levels/{id}`, `POST /inventory-levels/{id}/adjust`, `POST /orders` | | [`HOSTED_CALL_REFUSED`](/errors/hosted-call-refused) | 401 | every operation | | [`HTML_NOT_ALLOWED`](/errors/html-not-allowed) | 400 | `PATCH /collections/{id}`, `PATCH /products/{id}`, `POST /collections`, `POST /products` | | [`IDEMPOTENCY_KEY_INVALID`](/errors/idempotency-key-invalid) | 400 | 28 operations | | [`IDEMPOTENCY_KEY_IN_USE`](/errors/idempotency-key-in-use) | 409 | 28 operations | | [`IDEMPOTENCY_KEY_REUSED`](/errors/idempotency-key-reused) | 422 | 28 operations | | [`IDEMPOTENCY_UNAVAILABLE`](/errors/idempotency-unavailable) | 503 | 28 operations | | [`IMAGE_FETCH_FAILED`](/errors/image-fetch-failed) | 400 | `POST /images` | | [`IMAGE_QUOTA_REACHED`](/errors/image-quota-reached) | 429 | `POST /images` | | [`IMAGE_TOO_LARGE`](/errors/image-too-large) | 400 | `POST /images` | | [`IMAGE_TYPE_NOT_ALLOWED`](/errors/image-type-not-allowed) | 400 | `POST /images` | | [`IMAGE_URL_NOT_ALLOWED`](/errors/image-url-not-allowed) | 400 | `POST /images` | | [`INSUFFICIENT_AT_LOCATION`](/errors/insufficient-at-location) | 409 | `POST /inventory-levels/{id}/adjust` | | [`INTERNAL_ERROR`](/errors/internal-error) | 500 | every operation | | [`INVALID_CURSOR`](/errors/invalid-cursor) | 400 | 13 operations | | [`INVALID_FIELDS`](/errors/invalid-fields) | 400 | 49 operations | | [`LOCATION_NOT_FOUND`](/errors/location-not-found) | 400, 404 | `POST /inventory-levels/{id}/adjust` | | [`MEDIA_URL_NOT_ALLOWED`](/errors/media-url-not-allowed) | 400 | `PATCH /collections/{id}`, `PATCH /products/{id}`, `PATCH /products/{id}/variants/{variantId}`, `POST /collections`, `POST /products` | | [`METHOD_NOT_ALLOWED`](/errors/method-not-allowed) | 405 | any operation, as a fallback | | [`NOT_FOUND`](/errors/not-found) | 404 | 40 operations | | [`OFFER_ARCHIVED`](/errors/offer-archived) | 409 | `PATCH /offers/{id}`, `POST /offers/{id}/codes`, `POST /offers/{id}/codes/bulk`, `POST /offers/{id}/pause`, `POST /offers/{id}/publish`, `POST /offers/{id}/resume` | | [`OFFER_CODE_INVALID`](/errors/offer-code-invalid) | 400 | `POST /offers/{id}/codes` | | [`OFFER_CODE_NOT_FOUND`](/errors/offer-code-not-found) | 404 | `POST /offers/{id}/codes/{codeId}/disable`, `POST /offers/{id}/codes/{codeId}/enable` | | [`OFFER_CODE_TAKEN`](/errors/offer-code-taken) | 409 | `POST /offers`, `POST /offers/{id}/codes` | | [`OFFER_ENDED`](/errors/offer-ended) | 409 | `POST /offers/{id}/pause`, `POST /offers/{id}/resume` | | [`OFFER_GONE`](/errors/offer-gone) | 409 | `POST /orders` | | [`OFFER_INVALID`](/errors/offer-invalid) | 400 | `PATCH /offers/{id}`, `POST /offers`, `POST /offers/{id}/publish` | | [`OFFER_KIND_IMMUTABLE`](/errors/offer-kind-immutable) | 400 | `PATCH /offers/{id}` | | [`OFFER_KIND_NOT_AVAILABLE`](/errors/offer-kind-not-available) | 400 | `POST /offers` | | [`OFFER_LIMIT_REACHED`](/errors/offer-limit-reached) | 409 | `POST /orders` | | [`OFFER_NOT_CODE_METHOD`](/errors/offer-not-code-method) | 409 | `POST /offers/{id}/codes`, `POST /offers/{id}/codes/bulk` | | [`OFFER_NOT_FOUND`](/errors/offer-not-found) | 404 | 9 operations | | [`OFFER_NOT_OWNED`](/errors/offer-not-owned) | 403 | 11 operations | | [`OFFER_NOT_PUBLISHED`](/errors/offer-not-published) | 409 | `POST /offers/{id}/pause`, `POST /offers/{id}/resume` | | [`OFFER_STORE_NOT_FOUND`](/errors/offer-store-not-found) | 404 | `POST /offers` | | [`OFFER_TARGET_NOT_FOUND`](/errors/offer-target-not-found) | 400 | `PATCH /offers/{id}`, `POST /offers` | | [`OFFER_TARGET_OUTSIDE_STORE`](/errors/offer-target-outside-store) | 400 | `PATCH /offers/{id}`, `POST /offers` | | [`OFFER_VERSION_CONFLICT`](/errors/offer-version-conflict) | 409 | `PATCH /offers/{id}`, `POST /offers/{id}/pause`, `POST /offers/{id}/publish`, `POST /offers/{id}/resume` | | [`ORDER_DUPLICATE`](/errors/order-duplicate) | 409 | `POST /orders` | | [`ORDER_LIMIT_REACHED`](/errors/order-limit-reached) | 403 | `POST /orders/{id}/confirm` | | [`ORDER_NOT_EDITABLE`](/errors/order-not-editable) | 409 | `PATCH /orders/{id}`, `POST /orders/{id}/cancel`, `POST /orders/{id}/confirm`, `POST /orders/{id}/schedule` | | [`ORDER_PRICING_LOCKED`](/errors/order-pricing-locked) | 400 | `PATCH /orders/{id}` | | [`ORDER_STATUS_CONFLICT`](/errors/order-status-conflict) | 409 | `POST /orders/{id}/cancel`, `POST /orders/{id}/confirm`, `POST /orders/{id}/schedule` | | [`PAYLOAD_TOO_LARGE`](/errors/payload-too-large) | 413 | 22 operations | | [`PERMISSION_DENIED`](/errors/permission-denied) | 403 | 54 operations | | [`PLAN_FEATURE_REQUIRED`](/errors/plan-feature-required) | 403 | `GET /inventory-levels`, `GET /inventory-levels/{id}`, `POST /inventory-levels/{id}/adjust` | | [`PLAN_LIMIT_REACHED`](/errors/plan-limit-reached) | 403 | `PATCH /offers/{id}`, `POST /offers/{id}/publish`, `POST /offers/{id}/resume` | | [`PRODUCT_DEFAULT_VARIANT`](/errors/product-default-variant) | 409 | `PATCH /products/{id}/variants/{variantId}` | | [`PRODUCT_OPTIONS_REQUIRE_VARIANTS`](/errors/product-options-require-variants) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_OPTION_DUPLICATE_NAME`](/errors/product-option-duplicate-name) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_OPTION_DUPLICATE_VALUE`](/errors/product-option-duplicate-value) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_OPTION_NOT_FOUND`](/errors/product-option-not-found) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_OPTION_NO_VALUES`](/errors/product-option-no-values) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_OPTION_VALUE_EMPTY`](/errors/product-option-value-empty) | 400 | `PATCH /products/{id}` | | [`PRODUCT_OPTION_VALUE_NOT_FOUND`](/errors/product-option-value-not-found) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_SLUG_INVALID`](/errors/product-slug-invalid) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_SLUG_TAKEN`](/errors/product-slug-taken) | 409 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_VARIANT_COMBINATION_MISMATCH`](/errors/product-variant-combination-mismatch) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_VARIANT_COUNT_MISMATCH`](/errors/product-variant-count-mismatch) | 400 | `PATCH /products/{id}`, `PATCH /products/{id}/variants/{variantId}`, `POST /products` | | [`PRODUCT_VARIANT_DUPLICATE_COMBINATION`](/errors/product-variant-duplicate-combination) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_VARIANT_DUPLICATE_OPTION`](/errors/product-variant-duplicate-option) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_VARIANT_INVALID_VALUE`](/errors/product-variant-invalid-value) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_VARIANT_MISSING_COMBINATIONS`](/errors/product-variant-missing-combinations) | 400 | `PATCH /products/{id}`, `PATCH /products/{id}/variants/{variantId}`, `POST /products` | | [`PRODUCT_VARIANT_MISSING_OPTION`](/errors/product-variant-missing-option) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_VARIANT_SELECTED_COUNT`](/errors/product-variant-selected-count) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_VARIANT_UNKNOWN_OPTION`](/errors/product-variant-unknown-option) | 400 | `PATCH /products/{id}`, `POST /products` | | [`PRODUCT_WRITE_TIMEOUT`](/errors/product-write-timeout) | 400 | `PATCH /products/{id}`, `PATCH /products/{id}/variants/{variantId}`, `POST /products` | | [`RATE_LIMITED`](/errors/rate-limited) | 429 | every operation | | [`REQUEST_FAILED`](/errors/request-failed) | 4xx | any operation, as a fallback | | [`SERVICE_UNAVAILABLE`](/errors/service-unavailable) | 503 | every operation | | [`STORE_REQUIRED`](/errors/store-required) | 400 | `POST /orders` | | [`STORE_SCOPE_FORBIDDEN`](/errors/store-scope-forbidden) | 460 | 17 operations | | [`SUBSCRIPTION_REQUIRED`](/errors/subscription-required) | 422 | `GET /inventory-levels`, `GET /inventory-levels/{id}`, `POST /inventory-levels/{id}/adjust` | | [`UNAUTHORIZED`](/errors/unauthorized) | 401 | any operation, as a fallback | | [`UNPROCESSABLE_ENTITY`](/errors/unprocessable-entity) | 422 | any operation, as a fallback | | [`UNSUPPORTED_MEDIA_TYPE`](/errors/unsupported-media-type) | 415 | 22 operations | | [`VALIDATION_FAILED`](/errors/validation-failed) | 400 | 53 operations | ## Token endpoint errors `POST /apps/oauth/token` answers OAuth errors, `{ "error": "…", "error_description": "…" }`, never problem+json. | `error` | HTTP | | - | - | | `invalid_client` | 401 | | `invalid_grant` | 400 | | `invalid_request` | 400, 413 | | `slow_down` | 429 | | `temporarily_unavailable` | 503 | | `unauthorized_client` | 400 | | `unsupported_grant_type` | 400 | # Getting started Source: https://developers.tassyir.io/guides/getting-started Create an app with npm create tassyir-app, run it behind an https tunnel, and install it on a dev store. You want an app running on your machine, installed on a test business, with its page open in the Tassyir dashboard. This guide gets you there in about fifteen minutes. The app you start from shows a business's pending orders, keeps one setting in Tassyir, and logs the webhooks it receives. ## What you need * **Node 22.18 or later.** The starter runs its TypeScript directly on Node. * **A Tassyir account** at [app.tassyir.io](https://app.tassyir.io). * **A tunnel** that gives your machine a public **https** address on port 443, such as [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) or [ngrok](https://ngrok.com). Tassyir only calls https addresses on port 443 with a public hostname: never `localhost`, an IP address or another port. A command-line tool, `tassyir`, is coming to do the console steps below from a terminal. Until it ships, this guide uses the developer console in the dashboard. ## 1. Create the project ```bash theme={null} npm create tassyir-app@latest review-requests ``` It writes `review-requests/` and prints the next steps. The handle (the folder's name by default) is your app's permanent, unique name on Tassyir: it becomes the `handle` in `tassyir.app.toml`. Pass options after `--`: ```bash theme={null} npm create tassyir-app@latest review-requests -- --name "Review Requests" --json ``` The project is ready for coding agents: it has an `AGENTS.md`, a `CLAUDE.md` and a Claude Code skill that explains the Tassyir contract. Then install and set up: ```bash theme={null} cd review-requests npm install npm run setup ``` `npm run setup` writes `.env` from `.env.example`, with a new random key that encrypts the tokens the app stores. ## 2. Give your server a public address Start the tunnel to port 3000 and leave it running: ```bash theme={null} cloudflared tunnel --url http://localhost:3000 ``` Copy the https address it prints into `hosting.url` in `tassyir.app.toml` (no trailing `/`), then check the manifest with the Tassyir API's own validator: ```bash theme={null} npm run validate ``` A quick tunnel's address changes every time it starts, and every change is a new version of your manifest that the dev store must accept again. A tunnel that keeps its hostname (a named tunnel on your own domain, a reserved ngrok domain) saves that. ## 3. Create the app in the developer console 1. Open [app.tassyir.io/en/developers](https://app.tassyir.io/en/developers) and turn on developer mode. It is free, on every plan. 2. **Create an app** and choose "Your own server". Its handle must be exactly the `handle` of `tassyir.app.toml`. It never changes afterwards. 3. In the app's **Credentials** tab, create a client secret. It is shown once: copy it now. 4. Fill in `.env`: * `TASSYIR_CLIENT_ID`: the client id (`app_…`), on the Credentials tab; * `TASSYIR_CLIENT_SECRET`: the secret (`tsy_cs_…`); * `TASSYIR_APP_ID`: the app's numeric id, the number at the end of the app's address in the console (`/developers/apps/`). Webhooks name your app by this id; it is not the client id. 5. In the **Versions** tab, upload `tassyir.app.toml`. Your first version is current at once: custom apps are not reviewed. Never commit `.env`, and never put the secret in your page or a URL. ## 4. Create a dev store In the console, open **Dev stores** and create one. A dev store is a business for testing: about 30 products, 200 orders in every state across the 58 wilayas, and buyers with fake phone numbers. It costs nothing, and nothing it does reaches the outside world: carriers, ad pixels, WhatsApp and payments are switched off, and its storefront takes no orders. See [Dev stores](/guides/dev-stores). ## 5. Start the server ```bash theme={null} npm run dev ``` It listens on `http://localhost:3000`, restarts when `src/` changes, and rebuilds the page. If a setting is missing, it names it and says where to find it. ## 6. Install the app On the app's **Overview** tab in the console, copy the install link, open it, pick your dev store, and install. The install screen lists the scopes your manifest asks for, with your reason for each ([Scopes and consent](/guides/scopes-and-consent)). The dashboard then opens your page: the dev store's pending orders, and a setting saved in Tassyir through the dashboard's save bar. Your page asked App Bridge for an ID token, your server verified it and exchanged it for an access token, then called the Admin API ([Tokens](/guides/tokens)). ## 7. Watch webhooks Create an order in the dev store's dashboard: your server logs `orders/created`. In the console, the **Installs** tab sends a test delivery to your endpoint, and the **Webhook deliveries** tab shows every attempt and replays one ([Webhooks](/guides/webhooks)). ## Changing the manifest Whenever you change `tassyir.app.toml` (a scope, a webhook topic, the address): 1. `npm run validate`; 2. upload it in the **Versions** tab as a new version; 3. accept the update on the dev store: **Apps**, **Installed apps**, **Update**. An installation stays on the version it accepted until someone accepts the new one, and a version that adds a scope or a new address asks for consent again. See [Scopes and consent](/guides/scopes-and-consent#new-versions-and-updates). ## Deploying Any host that runs Node 22.18 or later behind https on port 443 works: 1. on the host, `npm ci`, `npm run build`, then `npm start`, with the variables of `.env` set in the host's environment (never upload `.env`); 2. set `hosting.url` to the host's address, upload the manifest, and accept the update. The starter keeps its tokens in one file on disk, so run one process with a persistent disk. To run several, move the tokens to a shared database and add a refresh lock ([Tokens](/guides/tokens#refresh-safely)). ## When something fails | You see | Do | | - | - | | The page says "Open this page from the Tassyir dashboard" | It runs outside the dashboard's frame: open it from **Apps** in the dashboard. | | The dashboard says the app did not load | Is the tunnel up, and is its address the manifest's `hosting.url`, uploaded and accepted? | | `ID_TOKEN_UNAVAILABLE` in the browser console | Tassyir cannot sign ID tokens right now; nothing in your app can fix it. Try again later. | | `invalid_client` from the token endpoint | Check the client id and secret. A revoked secret, or one rotated more than 24 hours ago, is refused. | | 403 `PERMISSION_DENIED` | Add the scope its `fix` names to `[access]`, upload, and accept the update. | | Webhooks refused with `WEBHOOK_WRONG_APP` | `TASSYIR_APP_ID` must be the numeric app id, not the client id. | | Webhooks refused with `WEBHOOK_UNKNOWN_INSTALLATION` | Open the app's page once, or wait for `app/installed`: Tassyir delivers the refused event again. | ## Next * [The manifest](/guides/manifest): every field of `tassyir.app.toml`. * [Admin API conventions](/guides/admin-api): lists, errors, retries and rate limits, before you call more of the API. * [Webhooks](/guides/webhooks): the topics, and handling them safely. # Build apps for Tassyir Source: https://developers.tassyir.io/guides/index What a Tassyir app is, the parts of the app contract, and where each guide fits. [Tassyir](https://tassyir.io) is the back office of e-commerce merchants in Algeria: orders (mostly cash on delivery), products and stock, delivery carriers, customers, and several stores per business. An app adds to it. A merchant installs your app on a business, and the app then reads and changes that business's data, receives its events, and can show its own pages inside the dashboard. ## What an app is **An installed app is a team member of the business.** It acts with exactly the scopes the merchant granted, only in the stores the merchant gave it, and under the merchant's plan: a plan limit refuses an app the way it refuses the merchant. The changes an app makes to orders, the catalogue and stock are written to the business's activity log under the app's name. An app runs on **your own server** today (`hosting.mode = "external"` in the manifest). [Tassyir Hosting](/guides/tassyir-hosting), where an app runs on Tassyir with no server to manage, opens later; an app moves there with the same manifest, API and webhooks. Apps are **custom apps** for now: one installs on the businesses its developer owns, and on one other business the developer names. A public catalogue of apps comes later. ## The parts | Part | What it is | Guide | | - | - | - | | Manifest | `tassyir.app.toml`: what the app is, the scopes it asks for, its address, page and webhooks | [The manifest](/guides/manifest) | | Scopes and consent | What a merchant grants on install, and how a new version asks again | [Scopes and consent](/guides/scopes-and-consent) | | Tokens | ID tokens, offline and online access tokens, refresh tokens, install codes | [Tokens](/guides/tokens) | | Embedded pages | Your page inside the dashboard, talking to it through App Bridge | [Embedded pages and App Bridge](/guides/embedded-pages) | | Webhooks | Signed deliveries of the business's events | [Webhooks](/guides/webhooks) | | Admin API | The versioned REST API an app calls | [Admin API conventions](/guides/admin-api) | | Orders | Read, create, edit, confirm, cancel and comment on orders | [Orders](/guides/orders) | | Products and collections | The catalogue, its images and collections | [Products and collections](/guides/products-and-collections) | | Stock | Stock levels, and correcting a count | [Stock](/guides/stock) | | Customers | Buyers and their delivery record, and the contact data scope | [Customers and contact data](/guides/customers) | | Offers | The merchant's pricing offers, read-only, with their webhooks | [Offers](/guides/offers) | | Write offers and codes | Run offers of your own: influencer codes in bulk, redemptions, app-owned offer types | [Write offers and codes](/guides/write-offers) | | Stores and delivery | A business's stores, carriers and delivery prices | [Stores and delivery](/guides/stores-and-delivery) | | App data | Values your app keeps in Tassyir, next to the merchant's resources | [App data](/guides/app-data) | | Storefront blocks | Not live yet: HTML your app adds to store pages, a Liquid template Tassyir checks and renders | [Storefront blocks](/guides/storefront-blocks) | | Dev stores | Test businesses with synthetic data, where nothing reaches the outside world | [Dev stores](/guides/dev-stores) | | Tassyir Hosting | Running the app on Tassyir: deploying, the scan, what each request receives | [Tassyir Hosting](/guides/tassyir-hosting) | | Compliance and uninstall | What happens when a merchant uninstalls, and the buyer data requests an app must honour | [Compliance and uninstall](/guides/compliance-and-uninstall) | | Publishing | Listing the app in the marketplace: the listing, the terms, the review checks, ratings | [Publishing to the marketplace](/guides/publishing) | | Errors | Every error code, and what to do about the common ones | [Errors](/guides/errors) | Start with [Getting started](/guides/getting-started): it creates an app, runs it on your machine and installs it on a dev store. ## The packages | Package | Use it for | | - | - | | [`create-tassyir-app`](https://github.com/tassyir/apps-sdk/tree/main/packages/create-tassyir-app) | `npm create tassyir-app`: a working app to start from | | [`@tassyir/app`](https://github.com/tassyir/apps-sdk/tree/main/packages/app) | Your server: tokens, ID-token and webhook verification, the typed Admin API client | | [`@tassyir/app-bridge`](https://github.com/tassyir/apps-sdk/tree/main/packages/app-bridge) | Your page in the dashboard: ID tokens, toasts, modals, the save bar, navigation, pickers | | [`@tassyir/app-manifest`](https://github.com/tassyir/apps-sdk/tree/main/packages/app-manifest) | Validating `tassyir.app.toml` with the same code the Tassyir API runs | | [`tassyir`](https://github.com/tassyir/apps-sdk/tree/main/packages/cli) | The command line: sign in, create apps, validate and deploy, preview blocks, submit | | [`@tassyir/storefront-blocks`](https://github.com/tassyir/apps-sdk/tree/main/packages/storefront-blocks) | Writing and testing storefront blocks with the code the Tassyir API runs (not live yet) | ## Addresses | What | Where | | - | - | | Admin API | `https://api.tassyir.io/apps/2026-10/` | | Token endpoint | `POST https://api.tassyir.io/apps/oauth/token` | | ID-token public keys | `GET https://api.tassyir.io/apps/.well-known/jwks.json` | | Webhook public keys | `GET https://api.tassyir.io/apps/webhook-keys` | | Your page in the dashboard | `https://app.tassyir.io/{locale}/dashboard/{businessId}/apps/{handle}/{path}` | | Developer console | `https://app.tassyir.io/en/developers` | ## Rules every app follows * **The ID token is the only proof of who is looking.** Your page's address says nothing Tassyir vouches for. Verify an ID token on your server ([Tokens](/guides/tokens)). * **Money is a decimal string** (`"2500.00"`) next to a `currency`. Never turn it into a JavaScript number ([Admin API conventions](/guides/admin-api)). * **Act on an error's `code`, and read its `fix`.** Every refusal has a stable code and one sentence that says what to do ([Errors](/guides/errors)). * **Webhooks arrive at least once, in any order.** Verify each one on its raw body, and skip a `webhook-id` you already handled ([Webhooks](/guides/webhooks)). * **Ask for the fewest scopes.** A merchant sees every scope, and a new one needs their consent again ([Scopes and consent](/guides/scopes-and-consent)). * **Never put a secret in a page, a URL, a log or a repository.** Client secrets and tokens live on your server. ## About the code in these guides The code is TypeScript on the published packages, and a check compiles every sample against them. Server code runs on Node 22.18 or later and on Workers-like runtimes; page code runs in the browser. To keep samples short, some use names set up once elsewhere: | Name | What it is | | - | - | | `tassyir` | Your server's `TassyirApp`, from `createTassyirApp` ([Tokens](/guides/tokens)) | | `admin` | An Admin API client for one installation, `tassyir.admin(installationId)` ([Admin API](/guides/admin-api)) | | `installationId` | The installation your code is working for | | `bridge` | Your page's App Bridge, from `createAppBridge()` ([Embedded pages](/guides/embedded-pages)) | Every API version is dated. These guides describe version `2026-10`. # The manifest Source: https://developers.tassyir.io/guides/manifest Describe your app in tassyir.app.toml, validate it with the API's own validator, and upload it as a version. Your app is described by one file, `tassyir.app.toml`: its name and handle, the scopes it asks for and why, where it runs, its page in the dashboard, the webhooks it receives and the values it keeps in Tassyir. You upload the file as a **version**; merchants consent to a version, and their installation stays on it until they accept a newer one. ## An example An app on your own server with a page in the dashboard: ```toml theme={null} name = "Review Requests" handle = "review-requests" api_version = "2026-10" [access] scopes = ["read_orders", "read_products"] optional_scopes = ["read_customer_contact"] [access.scope_reasons] read_orders = "Find delivered orders to ask for a review" read_products = "Show the product in the review request" read_customer_contact = "Send the review request to the buyer's phone" [hosting] mode = "external" url = "https://reviews.example.com" [admin] page = "/" [[webhooks]] topics = ["orders/delivered", "app/uninstalled"] uri = "/webhooks" [[webhooks]] topics = ["customers/data_request", "customers/redact", "business/redact"] uri = "/webhooks/privacy" [[data.definitions]] owner = "product" key = "rating" type = "json" visibility = "app" ``` A headless app (no page) gets its first tokens from an install code sent to its `auth.callback_url`, and may send a webhook to another host: ```toml theme={null} name = "Sheets Sync" handle = "sheets-sync" api_version = "2026-10" [access] scopes = ["write_orders", "read_products"] scope_reasons = { write_orders = "Create orders from the rows of your sheet", read_products = "Match sheet rows to your products" } [hosting] mode = "external" url = "https://sync.example.com/tassyir" [auth] callback_url = "https://sync.example.com/tassyir/callback" [[webhooks]] topics = ["orders/created", "products/updated"] uri = "/hooks" [[webhooks]] topics = ["customers/data_request", "customers/redact", "business/redact"] uri = "https://privacy.example.com/tassyir" ``` ## Fields | Field | Rule | | - | - | | `name` | Required. 1 to 50 characters on one line: no control, line-break or invisible characters (bidi controls, zero-width), no spaces around it. Arabic and English listing names are set in the console. | | `handle` | Required. 3 to 40 lowercase letters and digits, in runs joined by single hyphens. Unique on Tassyir, never changes, and must equal the handle of the app in the console. Some names are reserved (`www`, `api`, `tassyir`, …). | | `api_version` | Required. `2026-10`. | | `access.scopes` | The scopes the app needs ([Scopes and consent](/guides/scopes-and-consent)). Each once. | | `access.optional_scopes` | Scopes the merchant may tick. Never also in `scopes`, and never one a required scope already implies. | | `access.scope_reasons` | One sentence per requested scope, required and optional, shown on the install screen: 10 to 200 characters, the `name` text rules. No reason for a scope you do not request. | | `hosting.mode` | Required. `external`: the app runs on your server. `tassyir`: the app runs on [Tassyir Hosting](/guides/tassyir-hosting), whose uploads are refused until it opens. | | `hosting.url` | Required for `external`. `https://host` or `https://host/path`, at most 512 characters: no port (443 only), no user or password, no query, fragment or trailing `/`. The host is a public lowercase hostname: no IP address, no private name (`localhost`, `.local`, `.test`, …), nothing on Tassyir's domains. | | `hosting.allowed_hosts` | Required for `tassyir` (an empty list when the code calls no other service), refused for `external`. At most 20 hosts the app's code calls, each once: a lowercase hostname of two labels or more, or `*.` + one for its subdomains. No scheme, port, path, IP address, private name or Tassyir domain: a hosted app calls Tassyir through `ctx.props.TASSYIR`. | | `auth.callback_url` | Optional, `external` only. Where a headless app receives its install code ([Tokens](/guides/tokens#headless-apps-the-install-code)). The `hosting.url` rules, but a trailing `/` is allowed. | | `admin.page` | Optional. Your page in the dashboard: a path on `hosting.url` (on a hosted app, its own address), starting with `/` (not `//`), at most 256 characters, no query, fragment, backslash, space, `.` / `..` / empty segment or encoded slash; on a hosted app, never `/.tassyir` or below. Omit it for a headless app. | | `webhooks` | At most 10 entries, each `topics` (one or more) and `uri`. A topic is subscribed once in the whole manifest, and needs the scope that reads its resource ([Webhooks](/guides/webhooks#topics)). `uri` is a path on `hosting.url` (the `admin.page` rules), or for an external app an absolute https URL (the `hosting.url` rules). | | `data.definitions` | At most 50 keys of [app data](/guides/app-data): `owner` (`business`, `store`, `product`, `order`, `customer`), `key` (`[a-z][a-z0-9_]{0,63}`, unique per owner), `type` (`string`, `number`, `integer`, `boolean`, `json`), `visibility` (`app`, `merchant`, `storefront`). | | `data.definitions[].label` | Optional, `{ ar = "…", en = "…" }`: the name the dashboard shows next to a value. Not live yet, see [Marketplace fields](#marketplace-fields-not-live-yet). | | `storefront.blocks` | Storefront blocks: see [Marketplace fields](#marketplace-fields-not-live-yet). Not live yet. | | `offers.types` | Offer types a merchant can create from your app: see [Offer types](#offer-types). Needs `write_offers`. | | `admin.actions` | Menu entries in the dashboard: see [Marketplace fields](#marketplace-fields-not-live-yet). Not live yet. | | `app_proxy` | Part of the format, refused until the app proxy opens. | * **Every table is strict:** a key that is not a manifest field is an error, and the error's `fix` names the field you probably meant. * **Size:** at most 64 KB once parsed into JSON. Lengths count UTF-16 code units. * **Paths are joined to `hosting.url`**, its own path kept: with `url = "https://sync.example.com/tassyir"`, `uri = "/hooks"` is `https://sync.example.com/tassyir/hooks`. A hosted app's paths are joined to its own address, `https://{handle}.tassyir-apps.com`. * **Every host your data reaches** (`hosting.url`, an absolute webhook `uri`, `auth.callback_url`) is shown to the merchant on the install screen. ## Offer types An app that runs offers of its own ([Write offers and codes](/guides/write-offers)) can declare **kinds of offer** the merchant creates from your app, under "From your apps" on the Offers page: ```toml theme={null} [[offers.types]] handle = "mix-match" class = "product" path = "/offers/mix-match" label = { ar = "اخلط واختر", en = "Mix and match" } description = { ar = "القطعة الثالثة مجانًا", en = "The third piece free" } ``` At most 10 types, each with: | Field | Rule | | - | - | | `handle` | Required. 1 to 40 lowercase letters, digits and hyphens, starting with a letter or digit. Once per manifest (`MANIFEST_OFFER_TYPE_HANDLE_DUPLICATE`) | | `class` | Required. `product`, `order` or `delivery`: the step the offers of the type price in | | `path` | Required. The page of your app that configures one offer of the type: a path on your origin (the `admin.page` rules, at most 200 characters; on a hosted app never `/.tassyir` or below) | | `label` | Required. `{ ar = "…", en = "…" }`, 1 to 50 characters each (the `name` text rules) | | `description` | Optional. `{ ar, en }`, 1 to 200 characters each | Declaring any type needs the `write_offers` scope, in `scopes` or in `optional_scopes` (`MANIFEST_OFFER_TYPES_SCOPE_MISSING`). The type declares what the merchant can pick; the rule that prices each offer is written through the Admin API, with `kind = "app"` ([Your own offer types](/guides/write-offers#your-own-offer-types)). ## Marketplace fields (not live yet) These fields belong to the marketplace, which is **not live yet**. The validator in `@tassyir/app-manifest` already checks them, so you can prepare a manifest now, but the API refuses an upload that uses any of them until the marketplace opens: the fields are unknown to it, or `write_storefront` is not offered yet. This page will say when that changes. ```toml theme={null} name = "Review Stars" handle = "review-stars" api_version = "2026-10" [access] scopes = ["read_orders", "read_products", "write_storefront"] [access.scope_reasons] read_orders = "Find delivered orders to ask for a review" read_products = "Show each product's rating" write_storefront = "Add the rating block to your product pages" [hosting] mode = "external" url = "https://stars.example.com" [admin] page = "/" [[admin.actions]] handle = "ask-for-reviews" target = "orders.selection" label = { ar = "اطلب التقييمات", en = "Ask for reviews" } path = "/actions/ask" [[data.definitions]] owner = "product" key = "rating" type = "json" visibility = "storefront" label = { ar = "التقييم", en = "Rating" } [[storefront.blocks]] handle = "product-rating" template = "blocks/product-rating.liquid" stylesheet = "blocks/product-rating.css" name = { ar = "تقييم المنتج", en = "Product rating" } target = "product" data = ["product.rating"] [[storefront.blocks.settings]] key = "show_count" type = "boolean" label = { ar = "عرض عدد التقييمات", en = "Show the number of ratings" } default = true [[storefront.blocks.settings]] key = "heading" type = "string" label = { ar = "العنوان", en = "Heading" } default = { ar = "آراء الزبائن", en = "What buyers say" } ``` ### Labels Text a merchant reads in the dashboard or the store builder is a table with both languages, `{ ar = "…", en = "…" }`: a block's `name`, a setting's and an option's `label`, an admin action's `label` and a data definition's `label`. Each language follows the `name` rules (1 to 50 characters on one line, no invisible character, no spaces around it), and no other key is accepted. ### Storefront blocks A block is a template that Tassyir renders on the store's pages, where the merchant places it from the store builder. It runs no JavaScript: a block holds no script, and any key that would add code (`script`, `html`, `src`, `on…`) is refused. Blocks need the `write_storefront` scope, at most 20 a version. | Field | Rule | | - | - | | `handle` | Required. The `handle` rules; unique among the app's blocks. | | `template` | Required. A `.liquid` file of the app's files: a relative path of lowercase letters, digits, `_` and `-`, at most 200 characters. | | `stylesheet` | Optional. A `.css` file, the same path rules. | | `name` | Optional label: how the block is listed in the store builder. Write it: the review asks for one. | | `target` | `product` (the product page; the template can read the page's product) or `any` (any page but the order form's, without a product). Default `any`. | | `data` | At most 20 app data keys the template reads, each once, as `.`: declared in `[[data.definitions]]` with `visibility = "storefront"`, the owner `business`, `store` or `product`. Order and customer data never reach a store page. Product data needs `target = "product"`. | | `settings` | At most 20 `[[storefront.blocks.settings]]`: what the merchant sets for the block. | ### Block settings The template reads a setting as `block.settings.`. The store builder draws the form from the declaration and checks every value against it. | Field | Rule | | - | - | | `key` | Required. 1 to 40 lowercase letters, digits and underscores, starting with a letter; unique in the block. | | `type` | Required. `string`, `text`, `boolean`, `integer`, `number`, `select`, `color`, `image`, `url`, `product` or `collection`. | | `label` | Required label: the field's name in the store builder. | | `default` | Optional, a value of the type (below). | | `options` | `select` only, and required there: 2 to 20 `{ value, label }`. A `value` is 1 to 40 lowercase letters, digits, `_` and `-`, each once. | | `min`, `max` | `integer` and `number` only: the range, `min` at most `max`; whole numbers for an `integer`. | | `max_length` | `string` and `text` only: 1 to 1,000 characters (1,000 when absent). | | Type | `default` | | - | - | | `string` | One line of text, or `{ ar = "…", en = "…" }` for each language, within `max_length`. | | `text` | The same, line breaks allowed. | | `boolean` | `true` or `false`. | | `integer`, `number` | A whole number (`integer`) or a number, within `min` and `max`. | | `select` | The `value` of one of its options. | | `color` | `"#rrggbb"`. | | `url` | An `https://` link or a path of the store starting with `/`, at most 512 characters, no space. | | `image`, `product`, `collection` | None: the merchant picks one from their own store. | ```toml theme={null} [[storefront.blocks.settings]] key = "layout" type = "select" label = { ar = "الشكل", en = "Layout" } default = "grid" options = [ { value = "grid", label = { ar = "شبكة", en = "Grid" } }, { value = "list", label = { ar = "قائمة", en = "List" } }, ] ``` ### Admin actions `[[admin.actions]]` adds a menu entry on an order, product or customer page, or on a selection in the orders or products list. Choosing it opens one of the app's pages in the dashboard's app window, with the ids added to its address (`?id=` or `?ids=`, at most 50): read them again through the Admin API, where your scopes and the installation's stores apply. At most 10. | Field | Rule | | - | - | | `handle` | Required. The `handle` rules; unique among the app's actions. | | `target` | Required. `order.details`, `orders.selection`, `product.details`, `products.selection` or `customer.details`. It needs the read scope of its resource: `read_orders`, `read_products` or `read_customers`. | | `label` | Required label: the menu entry. | | `path` | Required. The page it opens: the `admin.page` rules. | An app with actions does not need `admin.page`. ### The app proxy `[app_proxy]` names the path on your app where Tassyir forwards requests made to the store's own address, `https://{store}/apps/{handle}/…`. It needs the `write_storefront` scope, and its `path` follows the `admin.page` rules. It is part of the format but refused until the app proxy opens (`MANIFEST_PROXY_NOT_AVAILABLE`), even by the validator. ```toml theme={null} [app_proxy] path = "/proxy" ``` ## Validate before you upload `@tassyir/app-manifest` is the validator the Tassyir API runs on every upload, so a manifest it accepts is one the API accepts, with the same errors in the same order. Parse the TOML with any TOML 1.0 parser, then validate: ```ts theme={null} import { readFileSync } from 'node:fs'; import { parse } from 'smol-toml'; import { validateManifest } from '@tassyir/app-manifest'; const result = validateManifest( parse(readFileSync('tassyir.app.toml', 'utf8')), ); if (!result.ok) { for (const error of result.errors) { console.error( `${error.code} at ${error.path || '(document)'}: ${error.fix}`, ); } process.exit(1); } console.log(result.manifest.access.scopes); // in catalogue order ``` A project made with `create-tassyir-app` runs this as `npm run validate` (`-- --json` for tools). It returns every error at once, never throws, and reads no file, clock or network. Each error has: | Field | Meaning | | - | - | | `code` | Stable: `MANIFEST_TOPIC_SCOPE_MISSING`, `MANIFEST_URL_INVALID`, … Never renamed. | | `path` | The field in the file: `webhooks[0].topics[1]`, `""` for the whole document. | | `message` | What is wrong, quoting at most 60 characters of your input. | | `fix` | One sentence saying what to change. | ```json theme={null} { "code": "MANIFEST_TOPIC_SCOPE_MISSING", "path": "webhooks[0].topics[1]", "message": "\"products/updated\" carries data that needs the \"read_products\" scope, which the manifest does not request.", "fix": "Add \"read_products\" to `access.scopes` (or `access.optional_scopes`), or remove \"products/updated\"." } ``` The same document always gives the same errors in the same order, whatever the order of its keys: one per field, at most 50, then `MANIFEST_TOO_MANY_ERRORS`. The package README lists every [error code](https://github.com/tassyir/apps-sdk/tree/main/packages/app-manifest#what-you-get-back). To check one field as it is typed (a handle in a form, say), use `validateManifestField('handle', value)`. ### In your editor The JSON Schema of the manifest ships with the package. In an editor that reads schema directives (Taplo, Even Better TOML), start the file with: ```toml theme={null} #:schema ./node_modules/@tassyir/app-manifest/schemas/manifest.2026-10.schema.json ``` The schema states the shape; the validator also checks the rules across fields and the exact URL and hostname rules, so a file the schema accepts can still be refused. ## Upload a version In the developer console, open your app's **Versions** tab and upload the file. The console sends the parsed manifest to the API, which validates it again and: * refuses it with every validator error (`APP_MANIFEST_INVALID`, each error in the shape above); * refuses a `handle` other than the app's (`APP_MANIFEST_HANDLE_MISMATCH`), a manifest identical to the latest version (`APP_VERSION_UNCHANGED`), and `hosting.mode = "tassyir"` while hosting is closed (`APP_HOSTING_NOT_AVAILABLE`); * otherwise numbers it (`1`, `2`, …) and makes it current at once. Custom apps are not reviewed: they install only on your own businesses and the one business you name. Limits per developer account: 50 apps (apps are never deleted), 30 version uploads an hour, and an upload of at most 128 KB. A hosted version also carries its code: `tassyir deploy` builds it, checks it and uploads both, then follows the scan every hosted version goes through ([Tassyir Hosting](/guides/tassyir-hosting#deploy-with-the-cli)). **Installations do not move by themselves.** A new version is offered to each installation as an update, and applies only when a member of that business accepts it. A version that adds a scope or a new host asks for consent again. See [Scopes and consent](/guides/scopes-and-consent#new-versions-and-updates). ## Versions of the format The manifest's shape belongs to an API version. Within a version, only new optional fields can be added; a release of `@tassyir/app-manifest` ships them, and an older release refuses them until you update it. # Offers Source: https://developers.tassyir.io/guides/offers Read the merchant's pricing offers with read_offers, follow them with the offers/* webhooks, and handle new kinds, store limits and codes you cannot see. An offer is a price rule a merchant sets on one of their stores: "buy 2 and save 900", free delivery over a threshold, a percentage off a product. It applies by itself, or when the buyer types one of its codes. An app reads the merchant's offers (a reports app that lines orders up with the offer that priced them, an ads app that shows the live deal in a campaign, an assistant that explains why a cart total is lower) and follows them with webhooks. With `read_offers` an app **reads** offers. With `write_offers` it also runs offers of its own, and their codes: that is [Write offers and codes](/guides/write-offers). An app never changes the merchant's own offers, and never applies an offer to an order itself. | Route | Does | Scope | | - | - | - | | `GET /offers` | List offers | `read_offers` | | `GET /offers/{id}` | Read one offer | `read_offers` | | `offers/created`, `offers/updated` | Webhooks (an offer) | `read_offers` | | `offers/deleted` | Webhook (an id) | `read_offers` | Reading offers needs no particular plan of the merchant (the plan limits how many offers can be live, not who can read them). `read_offers` is a normal scope and implies no other. Like any scope added after an app was installed, an installation does not hold it until the merchant accepts a version of your app that asks for it ([Scopes and consent](/guides/scopes-and-consent)). A call without it answers 403 `PERMISSION_DENIED`, with a `fix` that names the scope. ## The offer ```json theme={null} { "id": "741000000000002000", "storeId": "741000000000000100", "kind": "quantity", "method": "automatic", "title": "Buy 2, save 900", "label": { "ar": "عرض الكمية", "fr": null, "en": null }, "status": "active", "currency": "DZD", "quantity": { "priceMode": "total", "tiers": [ { "pieces": 2, "label": { "ar": "قطعتان", "fr": null, "en": null }, "total": "8900.00", "percentOff": null, "amountOffEach": null, "preselected": true, "freeDelivery": false } ] }, "freeDelivery": null, "productDiscount": null, "combinesWith": { "product": false, "order": true, "delivery": true }, "eligibility": { "audience": "everyone", "channels": [], "wilayas": [], "deliveryMethods": [] }, "targets": [{ "role": "buy", "type": "product", "id": "741000000000000500" }], "codeCount": 0, "totalUsageLimit": null, "perBuyerLimit": 1, "startsAt": "2026-10-01T00:00:00.000Z", "endsAt": null, "publishedAt": "2026-10-01T00:00:00.000Z", "pausedAt": null, "archivedAt": null, "appOwned": false, "createdAt": "2026-09-30T10:00:00.000Z", "updatedAt": "2026-10-02T10:00:00.000Z" } ``` * **`method`** is `automatic` (applies by itself) or `code` (the buyer types one of the offer's codes). * **`status`** is `draft` (never published), `scheduled` (starts later), `active`, `paused` or `ended` (past `endsAt`, or archived). It is worked out when the body is built, so a webhook body that is minutes old can be out of date: read the offer again when it matters. * **`label`** is what the buyer reads, per language (`ar`, `fr`, `en`; a language the merchant did not fill is `null`). `title` is the merchant's own name for the offer. * **`targets`** holds `{ role, type, id }` entries: `role` `buy` is what the offer applies to, and `type` is `all` (then `id` is `null`), `product`, `variant` or `collection`. Read a collection's products with `GET /products?collectionId=` ([Products and collections](/guides/products-and-collections)). * **`combinesWith`** says whether the offer may stack with other product, order and delivery offers. * **`eligibility`** limits who and where: `audience` is `everyone`, `first_order` or `returning`; `channels` are among `store`, `landing`, `shopify`, `dashboard` and `apps`, and `wilayas` and `deliveryMethods` (`home`, `desk`) narrow it further. **An empty list means no limit.** * **`totalUsageLimit`** and **`perBuyerLimit`** are the limits the merchant set (`null` = none). * **`appOwned`** is true for an offer that belongs to an installed app. Which installation is never shown. * Money is a decimal string next to `currency` (`DZD`). Timestamps are ISO 8601 in UTC. ## The kinds `kind` says which one of seven fields holds the rule. The other six are `null`: | `kind` | Field | Reads | | - | - | - | | `quantity` | `quantity` | `priceMode` `total` (each tier's `total` is the whole price of `pieces` pieces), `percent` (`percentOff` off each piece) or `amount_each` (`amountOffEach` off each piece). Tiers go up by `pieces`; at most one is `preselected`; a tier's `freeDelivery` waives delivery at that tier | | `free_delivery` | `freeDelivery` | The threshold: `thresholdKind` `pieces` (`thresholdPieces`) or `amount` (`thresholdAmount`), `null` while a draft has none; `delivery` `home`, `desk` or `both`; `wilayas` as two-digit codes, empty = every wilaya | | `product_discount` | `productDiscount` | `valueType` `percent` (`percent`, with `maxAmount` capping the saving per unit) or `fixed` (`amount` off each unit); `showCountdown` counts down to `endsAt` | | `order_discount` | `orderDiscount` | `valueType` `percent` (`percent`, with `maxAmount` capping the order's saving) or `fixed` (`amount` off the order); `minimumKind` `pieces` or `amount` (`minimumPieces`, `minimumAmount`), `null` = every order | | `bxgy` | `bxgy` | Buy X, get Y: `buyPieces` paid pieces and `getPieces` discounted ones per set; `getDiscountType` `percent` (`getPercent`, 100 = free) or `fixed_price` (`getPrice` per piece); `maxPerOrder` sets, `null` = as many as the cart holds. The buy and get products are the `targets` with role `buy` and `get` (no `get` target: the buy products themselves) | | `addon` | `addon` | A product offered next to the order: `priceType` `fixed_price` (`price`), `percent` or `amount_off` (`amountOff`); `maxQuantity`; `beforeSubmit` (in the order form) and `afterSubmit` (on the thank-you page). The add-on products are the `targets` with role `addon`, the products that trigger it role `buy` | | `app` | `appRule` | An offer of an app's own type: `{ typeHandle, rule }`, where `rule` is the declarative rule the app wrote ([Write offers and codes](/guides/write-offers#the-rule)). It is the app's JSON, so read it defensively | **`kind` can grow.** Tassyir adds kinds, and values to the other open fields (`priceMode`, `valueType`, `audience`, a target's `type`), inside API version `2026-10`. Write code that **ignores an offer of a kind it does not know** (all seven rule fields are `null` for it) instead of failing, and treat every field you do not know the same way: ```ts theme={null} import type { Offer } from '@tassyir/app'; /** One line for a campaign, or null for a kind this code does not know. */ export function describe(offer: Offer): string | null { switch (offer.kind) { case 'quantity': { const tier = offer.quantity?.tiers.find((t) => t.preselected); return tier ? `${String(tier.pieces)} pieces` : null; } case 'free_delivery': return 'Free delivery'; case 'product_discount': return offer.productDiscount?.percent ? `${String(offer.productDiscount.percent)}% off` : null; case 'bxgy': return offer.bxgy ? `Buy ${String(offer.bxgy.buyPieces)}, get ${String(offer.bxgy.getPieces)}` : null; case 'app': // The app's own rule: only its type is known to everyone. return offer.appRule?.typeHandle ?? null; default: // A kind added later. return null; } } ``` ## Codes and usage **Codes are not exposed.** `codeCount` is the number of codes that can still be used, and that is all an app learns about them. The codes and the redemptions of an offer are read only by the app that **owns** the offer ([Write offers and codes](/guides/write-offers)). **No buyer or phone is ever sent**, and using an offer at checkout sends no event. To know which orders an offer priced you need the orders themselves, under their own scope: an order's `discounts[]` names the offer that priced it ([Order money](/guides/orders#order-money)). ## List and read `GET /offers` lists the offers newest first, **drafts and ended offers included**, in pages ([Admin API conventions](/guides/admin-api#one-resource-and-lists)). Filters: | Parameter | Rule | | - | - | | `status` | One of `draft`, `scheduled`, `active`, `paused`, `ended`. Omitted: all of them | | `kind` | `quantity`, `free_delivery`, `product_discount`, `order_discount`, `bxgy`, `addon` or `app` | | `method` | `automatic` or `code` | | `storeId` | Only the offers of this store. A store outside the installation's is HTTP 460 `STORE_SCOPE_FORBIDDEN` | A cursor belongs to the filters it was made with: use the same four on every page, or `INVALID_CURSOR` answers 400. `GET /offers/{id}` answers one offer; an id that does not exist, is malformed, or belongs to a store outside the installation's is 404 `NOT_FOUND`. ```ts theme={null} for await (const offer of admin.paginate('/offers', { query: { status: 'active', method: 'automatic' }, })) { console.log(offer.storeId, offer.title, `${offer.kind}`); } const { data: offer } = await admin.get('/offers/{id}', { path: { id: '741000000000002000' }, }); console.log(offer.status, offer.endsAt); ``` ## Stores An offer belongs to exactly **one store**. An installation limited to selected stores reads the offers of those stores only, and the offers of any other store read as 404. Its webhooks follow the same rule: **every offer event carries the offer's store**, so a store-limited installation hears the events of its own stores and no others. An installation with every store hears them all. ## Webhooks Subscribe to the topics in the manifest, with the scope that reads them ([Webhooks](/guides/webhooks)): ```toml theme={null} [[webhooks]] topics = ["offers/created", "offers/updated", "offers/deleted"] uri = "/webhooks" ``` * `offers/created` is sent when an offer is created, with `data.offer` (the offer above). **A new offer is always a draft**: you hear `offers/updated`, with `changed` naming `status` and `publishedAt`, when it goes live. * `offers/updated` is sent when the offer reads differently: publishing, pausing, archiving, its dates, its rules, its targets or its codes. `changed` names the top-level fields that differ (`status`, `pausedAt`, `endsAt`, `quantity`, `targets`, `codeCount`, …), and `data.offer` is the offer as it is now. A change that alters nothing an app can read sends nothing. * `offers/deleted` is sent when an offer is deleted, with `data.offer = { "id": … }` and nothing else. Only an offer that was **never redeemed** can be deleted: a redeemed one is archived, which is `offers/updated` with `status: "ended"` and `archivedAt` set. Deliveries are at least once and unordered: deduplicate on `webhook-id`, and when two bodies of one offer arrive out of order, keep the one with the later `updatedAt`. ```ts theme={null} import type { WebhookEvent } from '@tassyir/app'; /** Keeps a local copy of the merchant's offers. */ export async function onOfferEvent( event: WebhookEvent, store: { save(id: string, offer: object): Promise; remove(id: string): Promise; }, ): Promise { switch (event.type) { case 'offers/created': case 'offers/updated': { const { offer } = event.data; // Keep the body even for a kind you do not understand: it is the truth. await store.save(offer.id, offer); break; } case 'offers/deleted': await store.remove(event.data.offer.id); break; default: break; } } ``` ## What is not here yet Creating offers, their codes (in bulk) and a list of redemptions, and offer types that belong to your app, are in [Write offers and codes](/guides/write-offers). No app code runs at checkout: an app-owned offer is a declarative rule that Tassyir evaluates. # Orders Source: https://developers.tassyir.io/guides/orders Read a business's orders, keep them in sync, and create, edit, confirm, cancel, schedule and comment on them like a team member. An app works orders the way a team member does: it reads them, creates orders from its own channel, fixes the buyer's details, confirms, cancels with a reason, schedules a call back and leaves comments. Every write runs the same checks as the dashboard button that does the same thing, and the dashboard shows your app's name on it ("Review Requests confirmed order \#1042"). | Route | Does | Scope | | - | - | - | | `GET /orders`, `GET /orders/{id}` | List and read orders | `read_orders` | | `GET /orders/{id}/comments` | Read an order's comments | `read_orders` | | `GET /cancel-reasons` | The business's cancel reasons | `read_orders` | | `POST /orders` | Create an order | `write_orders` | | `PATCH /orders/{id}` | Edit the buyer, delivery and notes | `write_orders` | | `POST /orders/{id}/confirm` | Confirm | `write_orders` | | `POST /orders/{id}/cancel` | Cancel, with an optional reason | `write_orders` | | `POST /orders/{id}/schedule` | Schedule a call back | `write_orders` | | `POST /orders/{id}/comments` | Add a comment | `write_orders` | Dispatching to a carrier, labels and returns are not in API version `2026-10`. ## The order ```json theme={null} { "id": "741000000000000001", "orderNumber": "1042", "storeId": "741000000000000100", "status": "confirmed", "waiting": false, "archived": false, "dispatched": false, "delivered": false, "returned": false, "currency": "DZD", "total": "5000.00", "shippingPrice": "500.00", "subtotal": "5000.00", "discountTotal": "500.00", "discounts": [ { "title": "Autumn sale", "code": null, "class": "product", "source": "offer", "amount": "500.00", "offerId": "741000000000000800" } ], "buyer": { "provinceCode": 16, "townOns": 1601, "name": "Amina B.", "phone": "+213555123456" }, "delivery": { "method": "home", "speed": "standard", "deliveryAgentId": null, "trackingNumber": null, "carrierStatus": null }, "items": [ { "id": "741000000000000011", "productId": "741000000000000500", "variantId": "741000000000000501", "packParentItemId": null, "name": "Robe kabyle", "variantName": "M", "sku": "RK-01-M", "quantity": 1, "unitPrice": "4500.00", "totalPrice": "4500.00", "originalPrice": "5000.00", "discountAmount": "500.00", "discounts": [ { "title": "Autumn sale", "code": null, "class": "product", "source": "offer", "amount": "500.00", "offerId": "741000000000000800" } ] } ], "source": "app:review-requests", "confirmedAt": "2026-10-11T09:12:44.120Z", "createdAt": "2026-10-11T08:00:00.000Z", "updatedAt": "2026-10-11T09:12:44.120Z" } ``` * **State:** `status` is the pipeline status, built in (`pending`, `confirmed`, `scheduled`, `canceled`, …) or one the merchant named: treat it as an open string. The flags `dispatched`, `delivered`, `returned`, `archived` and `waiting` say the rest. **A returned order keeps its status** (usually `pending`): read `returned`. * **Money:** `total`, `shippingPrice`, `subtotal`, `discountTotal`, and each line's `unitPrice`, `totalPrice`, `originalPrice` and `discountAmount` are decimal strings in `currency`; `discounts[]` says which offers priced the order ([Order money](#order-money)). * **Buyer:** `provinceCode` (the wilaya, 1 to 58) and `townOns` (the commune) always; `name`, `phone`, `phone2`, `email`, `address`, `address2`, and the order's `note` and `deliveryNote`, only with `read_customer_contact`. * **Delivery:** `method` (`home` or `stopdesk`), `speed`, the delivery agent, the tracking number and `carrierStatus`, the carrier's own wording. * **Lines:** `items[]`; a pack's component lines carry `packParentItemId`. * **Where it came from:** `source` (`shopify:`, `app:` for an order an app created, `null` for the storefront and the dashboard), `landingPageId`, `utm { source, medium, campaign, content }`. * **Other:** `exchangeOfOrderId`, `cancellation { reasonId, reasonLabel, source }`, and the times `scheduledFor`, `confirmedAt`, `dispatchedAt`, `deliveredAt`, `returnedAt`, `canceledAt`, `createdAt`, `updatedAt`. **Waiting orders.** An order past the plan's order allowance is created `waiting: true` and masked whatever your scopes (phones `**********`, notes `null`) until it unlocks, which sends `orders/updated` with `waiting`. You may read, edit, cancel, schedule and comment on it; confirming it is 403 `ORDER_LIMIT_REACHED`. ## Order money Read an order's money in this order: the goods before any offer, what the offers took off, what the buyer pays for delivery, and what the courier collects. | Field | Meaning | | - | - | | `subtotal` | The goods before every offer. Always present. An order with no discount data (older orders, exchanges, orders synced from another channel) reads `total − shippingPrice`. | | `discountTotal` | What the product and order discounts took off. `"0.00"` when none. It **leaves out** a delivery waiver. | | `shippingPrice` | What the buyer pays for delivery: `"0.00"` when it was waived. | | `total` | What the courier collects: `subtotal − discountTotal + shippingPrice`. | | `discounts[]` | Every discount the order got, one entry per title, code, class and source. | | `items[].originalPrice` | The catalogue unit price before any offer. `null` on a line no discount was worked out for (older orders, exchanges, synced orders). | | `items[].discountAmount` | The line's share of the product and order discounts. `"0.00"` when none. | | `items[].discounts[]` | The `product` entries that priced this line. | A `discounts[]` entry is `{ title, code, class, source, amount, offerId }`: * `title` is the offer or code's name, frozen when the order was placed: it does not follow a later rename. * `class` is `product` (taken off lines), `order` (taken off the whole order) or `delivery` (the delivery waiver, which is **not** part of `discountTotal`). * `source` is `offer`, `code`, `manual`, `app` or `import`; treat it as an open string. `code` is the code the buyer typed, if any. * `offerId` is `null` for a manual, imported or older quantity-tier discount. Read the offer through [Offers](/guides/offers) when it is set. **Lines are net.** A line's `unitPrice` and `totalPrice` are what was charged after the discounts; `originalPrice` is the gross unit price. For an order priced with discounts, `totalPrice = originalPrice × quantity − discountAmount`, `unitPrice = totalPrice / quantity`, and `discountTotal` is the sum of the lines' `discountAmount`. A discount that covers several lines is spread over them, so a line's `totalPrice` is its own amount, never the whole offer's. **"You saved"** is `discountTotal` plus the `amount` of the entries with `class: "delivery"`. Add them with a decimal library, never `parseFloat`. ```ts theme={null} const { data: order } = await admin.get('/orders/{id}', { path: { id: '741000000000000001' }, }); for (const item of order.items) { if (item.discountAmount === '0.00') continue; const offers = item.discounts.map((discount) => discount.title).join(', '); console.log( `${item.name}: ${item.originalPrice ?? '?'} → ${item.unitPrice ?? '?'} (${offers})`, ); } console.log('goods', order.subtotal, 'off', order.discountTotal); ``` The fields come with or without `read_customer_contact`, and on a waiting order: money carries no contact data. `fields` accepts `subtotal`, `discountTotal` and `discounts`. The `orders/*` webhook bodies carry the same fields, and a change to one of them (a discount added or removed on an edit) sends `orders/updated` with `changed` naming `subtotal`, `discountTotal` or `discounts`: if you switch over `changed`, keep a default branch. An abandoned checkout's lines carry `originalPrice`, `discountAmount` and `discounts`, but the checkout itself has no order-level money fields. ## Which orders you can reach `GET /orders` lists every order of the business your token may read: every stage, archived ones included, newest first. Not listed: abandoned checkouts still waiting for the merchant (they are `abandoned_checkouts/created`, and become orders when recovered) and the collected-goods rows Tassyir keeps for an exchange. Orders of stores outside your installation's are left out. Every route that names an order reads it that way first: any other id is **404 `NOT_FOUND`**, before anything is written. With an online token whose person may see only the orders assigned to them, you reach only those. ## List and sync | Filter | Rule | | - | - | | `status` | One status or several, comma-separated (`confirmed,scheduled`): 1 to 20, custom statuses by name. | | `createdSince` | ISO 8601 (`2026-10-01T00:00:00Z`; a date alone is midnight UTC): created at or after it. | | `updatedSince` | Changed at or after it, carrier tracking included. | | `storeId` | One of your installation's stores; another is 460. | Filters only narrow, and the cursor is bound to them. The list stays ordered by creation, so to sync **walk every page and keep the largest `updatedAt` you saw**, then start the next sync from it: ```ts theme={null} export async function syncOrders(since: string): Promise { let newest = since; for await (const order of admin.paginate('/orders', { query: { updatedSince: since, limit: 100 }, })) { // Upsert the order in your own store here, by id. if (order.updatedAt > newest) newest = order.updatedAt; } return newest; // where the next sync starts } ``` Between syncs, the `orders/*` webhooks tell you what changed ([Webhooks](/guides/webhooks#how-events-are-recorded)). ## Create an order ```ts theme={null} const created = await admin.request('post', '/orders', { body: { storeId: '741000000000000100', buyer: { name: 'Amina B.', phone: '+213555123456', provinceCode: 16, townOns: 1601, address: '12 rue Didouche Mourad', }, items: [ { productId: '741000000000000500', variantId: '741000000000000501', quantity: 2, }, ], delivery: { method: 'home', speed: 'standard' }, note: 'Call after 6pm', }, // One key per order of your channel: a retry never creates it twice. idempotencyKey: 'sheet-row-128', }); if (created.status === 201) console.log('created', created.data.data.orderNumber); else console.log( 'the same buyer already ordered these items', created.data.data.id, ); ``` | Field | Rule | | - | - | | `storeId` | Optional for an installation with one store or every store (then resolved from the items). Required with several selected stores (400 `STORE_REQUIRED`). | | `buyer` | Required, may be `{}`. `phone`, `phone2`: `+213`, then 5, 6 or 7, then 8 digits. `provinceCode` 1 to 58, `townOns` a commune code, `name` up to 200, `address` and `address2` up to 500, `email`. | | `items` | 1 to 50 lines of `productId`, optional `variantId`, `quantity` 1 to 999. Each product must be one you can list; same lines are merged. | | `delivery` | Optional. `method`: `home` (default) or `stopdesk` (the desk of the buyer's commune). `speed`: `standard` (default) or `economy`. | | `codes` | Optional, **one offer code at most** (`["SARA10"]`). A code that does not apply never fails the order: it is listed in `rejectedCodes` beside `data` ([Write offers and codes](/guides/write-offers#applying-a-code-to-an-order)). | | `note`, `deliveryNote` | Optional, up to 1,000 characters. | Anything else is refused, on purpose: an order is created `pending` (confirm it with the route below), priced from the catalogue and the business's delivery prices, never from your numbers, and carries no carrier, tracking, payment or attribution of your choosing. What happens is what happens to an order a merchant types in the dashboard: * **The duplicate rule.** A second order for the same phone and the same items, within 24 hours and not yet dispatched, is folded into the first: **200** with that order, nothing created. If you cannot read that order, 409 `ORDER_DUPLICATE`. * **An abandoned checkout becomes the order.** When the buyer (same phone) left a checkout sharing a product, the order takes the checkout's id. * **The business's distribution assigns it** to a team member, once. * **The plan's allowance admits it**: past it, the order is created `waiting`, and your answer is masked. * Its `source` is `app:`, and `orders/created` is sent once. * The storefront's anti-spam checks do not apply: your app is an authenticated member of the business, not a buyer's browser. Answers: **201** for a new order (or a recovered checkout), **200** for an existing one. With an `Idempotency-Key`, a retry replays the stored answer and its status. ## Edit, confirm, cancel, schedule ```ts theme={null} const id = '741000000000000001'; await admin.patch('/orders/{id}', { path: { id }, body: { buyer: { townOns: 1602 }, deliveryNote: null }, }); const { data: reasons } = await admin.get('/cancel-reasons'); const noAnswer = reasons.find((reason) => !reason.requireNote); await admin.post('/orders/{id}/schedule', { path: { id }, body: { scheduledFor: '2026-10-20T09:00:00Z' }, }); await admin.post('/orders/{id}/cancel', { path: { id }, body: noAnswer ? { reasonId: noAnswer.id } : { note: 'Buyer cancelled by phone' }, }); ``` * **`PATCH`** changes `buyer` (only the keys you send), `delivery` (`method`, `speed`), `note` and `deliveryNote` (`null` clears one). Not the status, the store or the items. A new commune, method or speed re-prices the delivery. A dispatched, delivered or returned order is 409 `ORDER_NOT_EDITABLE`. It sends `orders/updated`. * **`confirm`** (no body) checks the buyer's details for a carrier: a missing phone, wilaya or commune is a 400 naming `missingFields`. An empty address is filled from the commune. Sends `orders/confirmed`. Refusals: 403 `ORDER_LIMIT_REACHED` (a waiting order), 409 `ORDER_STATUS_CONFLICT` (its status does not allow it, already confirmed included). * **`cancel`** takes an optional `reasonId` (from `GET /cancel-reasons`) and `note` (up to 500). An archived or unknown reason is 400 `CANCEL_REASON_INVALID`; a reason that requires a note, without one, 400 `CANCEL_NOTE_REQUIRED`. Sends `orders/cancelled`. * **`schedule`** takes `scheduledFor`, in the future: the status becomes `scheduled`. An order already scheduled is 409 `ORDER_STATUS_CONFLICT`. Sends `orders/updated`. Every answer is the order, read again (`fields` trims it). ## Comments `GET /orders/{id}/comments` lists the team's notes on the order, newest first; `POST` adds one from your app (`{ "body": "…" }`, 1 to 1,000 characters). ```json theme={null} { "id": "741000000000007002", "orderId": "741000000000000001", "byThisApp": true, "body": "Risk score 12/100", "createdAt": "2026-10-12T08:00:00.000Z", "updatedAt": "2026-10-12T08:00:00.000Z" } ``` `byThisApp` marks your own comments; who else wrote one is not said. The team often types phone numbers into comments, so another author's `body` follows the notes rule: present only with `read_customer_contact`, and `null` while the order is waiting. Your own comments always carry their body. Comments send no webhook. ## Attribution Every write is recorded in the activity log under your app's name, whatever the token kind. An app's write never counts as a team member working the order, and earns no commission. ## Errors these routes add | Status | Code | When | | - | - | - | | 409 | `ORDER_NOT_EDITABLE` | The order is dispatched, delivered or returned. | | 409 | `ORDER_STATUS_CONFLICT` | Its status does not allow the move. | | 409 | `ORDER_DUPLICATE` | A create folded into an order you cannot read. | | 403 | `ORDER_LIMIT_REACHED` | Confirming a waiting order. | | 400 | `STORE_REQUIRED` | Several selected stores and no `storeId`. | | 400 | `CANCEL_REASON_INVALID`, `CANCEL_NOTE_REQUIRED` | The cancel reason rules. | | 400 | `DELIVERY_SPEED_UNAVAILABLE` | Economy is not priced for that destination. | See [Errors](/guides/errors) for the codes every route shares. # Products and collections Source: https://developers.tassyir.io/guides/products-and-collections Read and change the merchant's catalogue, bring images onto the Tassyir CDN, and organise collections. 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. | Route | Does | Scope | | - | - | - | | `GET /products`, `GET /products/{id}` | List and read products | `read_products` | | `POST /products` | Create a product | `write_products` | | `PATCH /products/{id}` | Change a product | `write_products` | | `PATCH /products/{id}/variants/{variantId}` | Change one variant | `write_products` | | `GET /collections`, `GET /collections/{id}` | List and read collections | `read_products` | | `POST /collections`, `PATCH /collections/{id}` | Create and change a collection | `write_products` | | `PATCH /collections/{id}/products` | Add and remove products | `write_products` | | `DELETE /collections/{id}` | Delete a collection | `write_products` | | `POST /images` | Bring an image onto the Tassyir CDN | `write_products` | No route deletes a product: set `active: false` to take one off the store. Stock is its own resource ([Stock](/guides/stock)), and so is the buy price (`read_costs`). ## The product ```json theme={null} { "id": "741000000000000500", "storeId": "741000000000000100", "name": "Robe kabyle", "slug": "robe-kabyle", "description": "

Faite main à Tizi Ouzou.

", "type": "PHYSICAL", "active": true, "visible": true, "currency": "DZD", "price": "4500.00", "compareAtPrice": "5200.00", "sku": "RK-01", "barcode": null, "image": "https://cdn.tassyir.io/images/0f1e2d3c/public", "images": ["https://cdn.tassyir.io/images/0f1e2d3c/public"], "weightKg": 0.6, "dimensionsCm": { "width": 30, "height": 5, "length": 40 }, "tracksInventory": true, "allowOutOfStockOrders": false, "collectionIds": ["741000000000000600"], "brandId": null, "options": [{ "name": "Taille", "values": ["S", "M"] }], "variants": [ { "id": "741000000000000501", "name": "M", "sku": "RK-01-M", "barcode": null, "price": "4700.00", "compareAtPrice": null, "image": null, "isDefault": false, "hidden": false, "options": [{ "name": "Taille", "value": "M" }], "createdAt": "2026-10-02T10:00:00.000Z", "updatedAt": "2026-10-02T10:00:00.000Z" } ], "createdAt": "2026-10-02T10:00:00.000Z", "updatedAt": "2026-10-03T10:00:00.000Z" } ``` * `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`): `
12 sent this week Send requests 120 messages left.
Order Total Status
#00001042 9.000 دج Pending
Skip buyers who returned an order Save
``` ```ts theme={null} import { createAppBridge } from '@tassyir/app-bridge'; import { followDashboardContext } from '@tassyir/ui'; // also defines every element const bridge = createAppBridge(); // The dashboard's language, direction and theme, on , now and on // every change. The elements and the tokens follow. followDashboardContext(bridge); document .querySelector('#settings')! .addEventListener('submit', async (event) => { event.preventDefault(); const settings = Object.fromEntries( new FormData(event.currentTarget as HTMLFormElement), ); // send `settings` to your server with a fresh ID token, then: await bridge.toast.show({ message: 'Settings saved', tone: 'success' }); }); ``` The fields submit with the `
` around them like native ones: `new FormData(form)` holds `sender`, `delay` and `skipReturns`. ## The dashboard's rules The elements keep DESIGN.md, the dashboard's design law. Your page keeps the rest: * **One ink button per view.** `variant="primary"` is the dashboard's dark call to action; put the view's one primary in the page header's actions. Everything else is `outline` (the default), `ghost` or `destructive`. * **Status is a ``**, never a coloured word or a badge of your own. * **No new colour.** Use the tokens (`var(--tsy-color-…)`), never a hex code. Cyan (`--tsy-color-brand-cyan`) is a highlighter: selection, progress, charts; never a button or a status. * **14px is the floor** for any text, and type is the dashboard's sans. * **Logical CSS only** (`margin-inline-start`, `padding-inline`, `text-align: start`), so the same rules serve Arabic and English. * **Money and dates** read as the dashboard writes them: `9.000 دج` in Arabic, `DZD 9,000` in English; dates `yyyy-MM-dd` (`yyyy-MM-dd HH:mm`). * **Modals, toasts, resource pickers, the save bar and the page title are not elements here.** They are App Bridge calls (`modal.open`, `toast.show`, `resource.pick`, `saveBar.show`, `titleBar.set`): the dashboard draws them, signed with your app's name. See [`@tassyir/app-bridge`](https://github.com/tassyir/apps-sdk/tree/main/packages/app-bridge#readme). ## Language, direction and theme Your page runs in the dashboard's frame and cannot see its settings: App Bridge sends them (`context.changed`: `locale`, `dir`, `theme`). `followDashboardContext(bridge)` applies the current context at once and every change after it, and returns the function that stops. It sets three attributes on ``: * `lang`: `ar` or `en`. The elements' own words follow it ("Back", "Loading", "Dismiss", "(opens in a new tab)": Arabic for `ar`, English otherwise). * `dir`: `rtl` or `ltr`. Every element is drawn with logical properties; arrows and carets mirror; arrow keys in a radio group follow the reading direction. * `data-theme`: `light` or `dark`. The tokens switch, with `color-scheme`. `applyDashboardContext({ locale, dir, theme }, root?)` sets them yourself, for example in a page opened outside the dashboard. Light is the default. A subtree with its own `data-theme` keeps that theme inside a page of the other one. The helpers take `@tassyir/app-bridge`'s bridge as it is but do not import it: any object with `context.current` and `on('context.changed', …)` works. ## Elements Importing `@tassyir/ui` defines every element (in a browser; on a server it only exports, so server rendering can import it). An attribute that takes a value ignores one it does not know and uses its default. Frameworks: React 19, Vue, Svelte and Lit set a prop as a property when the element has one and as an attribute otherwise; every property here that shares an attribute's name (`value`, `checked`, `form`, `prefix`, `type`, …) takes the attribute's value, so the markup above works as written. Events are DOM events (`input`, `change`, `tsy-dismiss`): where a framework does not bind them, add a listener with `addEventListener` (in React, through a ref). ### `` The dashboard's button: 36px, 12px corners, a 14px medium label. ```html theme={null} Save Settings ⋯ ``` | Attribute | Values | Meaning | | - | - | - | | `variant` | `outline` (default), `primary`, `ghost`, `destructive` | `primary` is the ink button, one per view. | | `size` | `md` (default, 36px), `sm` (32px), `lg` (40px) | | | `type` | `button` (default), `submit`, `reset` | `submit` submits the form around it, `reset` resets it. | | `disabled` | boolean | Dimmed, out of the tab order, clicks swallowed. | | `loading` | boolean | A spinner before the label; stays focusable, `aria-busy`, clicks swallowed. | | `href` | URL | A link that looks like a button. `target`, `rel` and `download` pass through. | | `target`, `rel`, `download` | as on `` | With `href` only. | | `full-width` | boolean | Fills its container's width. | | `square` | boolean | As wide as it is high, for one glyph. Give it an `aria-label`. | `aria-label`, `aria-pressed` and `aria-expanded` are passed to the inner control. A submit or reset acts after every `click` listener ran, unless one called `preventDefault()`. Slot: the label (a 16px `` may sit beside it). ### `` An underlined link in the colour of the text around it, ink on hover. ```html theme={null} Questions? Read the guide. ``` | Attribute | Values | Meaning | | - | - | - | | `href` | URL | The address. | | `external` | boolean | Opens in a new tab with `rel="noopener noreferrer"`, shows ↗ and says so to screen readers. | | `target`, `rel`, `download` | as on `` | | `aria-label` passes to the inner ``. In the dashboard, your app moving to another of its pages is `bridge.navigate({ path })`; leaving for a dashboard page is `bridge.navigate({ dashboard })`. ### `` A labelled input, or a text area with `multiline`. ```html theme={null} ``` | Attribute | Values | Meaning | | - | - | - | | `label` | text | The field's label. Always give one. | | `label-hidden` | boolean | Keeps the label for screen readers only (a search box). | | `name` | text | The form field's name. | | `value` | text | The default value; the `value` property is the current one. | | `type` | `text` (default), `email`, `number`, `password`, `search`, `tel`, `url` | `email`, `password`, `tel` and `url` are typed left to right in an Arabic page too. | | `hint` | text | A muted line under the field. | | `error` | text | The field's error, in red under it; marks the field invalid for assistive technology. | | `prefix`, `suffix` | text | A short unit inside the field, such as `DZD` or `دج`. | | `required`, `disabled`, `readonly` | boolean | | | `multiline` | boolean | A text area. | | `rows` | number | With `multiline`. | | `placeholder`, `autocomplete`, `inputmode`, `minlength`, `maxlength`, `min`, `max`, `step`, `pattern`, `spellcheck`, `enterkeyhint` | as on `

` unless you say otherwise. | | `count` | text | A count chip beside the title. | | `flush` | boolean | No padding in the body (a list or a table edge to edge). | Slots: the body; `actions`. Never put a card inside a card. ### `` The page's title, its one `

`, with an optional back link, a muted line of facts and the page's actions. ```html theme={null} Sara Bensalem · Oran Confirm ``` | Attribute | Values | Meaning | | - | - | - | | `heading` | text | The page's title (24px; 22px right to left). | | `back-href` | URL | Shows a back link above the title. | | `back-label` | text | The back link's words (default "Back", Arabic «رجوع»). | Slots: `meta`, `actions`. The dashboard's title bar already shows your app's name; `bridge.titleBar.set({ title })` adds the page's title beside it. ### `` The dashboard's table frame around a native `` you write inside it: a tinted frame, an optional header bar, a toolbar strip, the rows as a paper panel that scrolls sideways on a phone, and a footer. The table keeps its native semantics. ```html theme={null}
Order Total
#00001042 DZD 9,000
1–20 of 128
``` | Attribute | Values | Meaning | | - | - | - | | `heading` | text | The frame's title; also the table's name for screen readers, unless it has a caption, `aria-label` or `aria-labelledby`. | | `heading-level` | `2` (default) to `6` | | | `count` | text | A count chip beside the title. | | `density` | `compact` (default), `comfortable` | Roomier rows. | Slots: the ``; `actions`, `toolbar`, `footer`. On cells: `data-align="end"` aligns a number to the end, `data-wrap` lets a cell wrap. A row with `aria-selected="true"` is tinted. The table's styles are adopted into the document (or the shadow root it lives in), in the `tassyir-ui` cascade layer. ### `` The dashboard's only status treatment: a dot, a 12% tint of the tone, the words in the tone pushed toward ink. ```html theme={null} Delivered Called twice ``` | Attribute | Values | Meaning | | - | - | - | | `tone` | `neutral` (default), `success`, `warning`, `destructive`, `info`, `purple` | The semantic hue. | | `color` | a CSS colour | A merchant's own status colour, mixed the same way; overrides `tone`. | | `size` | `sm` (default), `md` | | | `hide-dot` | boolean | No dot. | Slot `icon`: a 16px glyph in place of the dot (a lock, say). ### `` A quiet tinted notice: a glyph, an optional heading, the message and actions. ```html theme={null} The carrier refused their phone numbers. See who ``` | Attribute | Values | Meaning | | - | - | - | | `tone` | `info` (default), `success`, `warning`, `critical` | `warning` and `critical` are announced (`role="alert"`), the others politely (`role="status"`). | | `heading` | text | A bold first line. | | `dismissible` | boolean | A close button. | Slots: the message; `actions`. Closing fires `tsy-dismiss` (bubbles, cancelable); unless a listener prevents it, the banner hides. `dismiss()` does the same from code. A passing confirmation ("Saved") is a toast: `bridge.toast.show`. ### `` What a list says when it has nothing yet. Keep it for a surface with something to teach; in a form, a plain button says more. ```html theme={null} Answers appear here once buyers reply. Send a test request ``` | Attribute | Values | Meaning | | - | - | - | | `heading` | text | The title. | | `heading-level` | `1` to `6` | Makes the title a heading for assistive technology (it is not, by default). | Slots: `media` (a glyph), the sentence, `actions`. ### `` A loading placeholder in the shape of what is coming. The dashboard shows skeletons, not spinners, while a page or a card loads. ```html theme={null} ``` | Attribute | Values | Meaning | | - | - | - | | `shape` | `line` (default), `block`, `circle` | | | `lines` | number (1 to 20) | Lines of text, the last one shorter (`line` only). | | `width` | a CSS length (a number is pixels) | | | `height` | a CSS length (a number is pixels) | | It is hidden from assistive technology; mark the region it stands in `aria-busy="true"` and clear it when the content arrives. The pulse stops under reduced motion. ### `` In-place loading: a button, a row. ```html theme={null} ``` | Attribute | Values | Meaning | | - | - | - | | `size` | `md` (default, 24px), `sm` (16px), `lg` (32px) | | | `label` | text | What screen readers hear (default "Loading", in the page's language). | ## Forms ``, ``, ``, `` and `` are form-associated custom elements: they submit with the native `` around them (`FormData`, a normal submit), take part in its validation (`required`, the input's own constraints, `setCustomValidity`), are reset with it and follow a disabled `
`. Enter in a one-line field does what it does in a native one: it clicks the form's first submit button (nothing happens while that button is disabled or `loading`, so a save in flight is not sent twice); with no submit button, it submits a form that has only that one field. * Label every field with its `label` attribute. A `