Skip to main content
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. An app never changes the merchant’s own offers, and never applies an offer to an order itself. 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). A call without it answers 403 PERMISSION_DENIED, with a fix that names the scope.

The offer

  • 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).
  • 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 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:

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). 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).

List and read

GET /offers lists the offers newest first, drafts and ended offers included, in pages (Admin API conventions). Filters: 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.

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):
  • 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.

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. No app code runs at checkout: an app-owned offer is a declarative rule that Tassyir evaluates.