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
methodisautomatic(applies by itself) orcode(the buyer types one of the offer’s codes).statusisdraft(never published),scheduled(starts later),active,pausedorended(pastendsAt, 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.labelis what the buyer reads, per language (ar,fr,en; a language the merchant did not fill isnull).titleis the merchant’s own name for the offer.targetsholds{ role, type, id }entries:rolebuyis what the offer applies to, andtypeisall(thenidisnull),product,variantorcollection. Read a collection’s products withGET /products?collectionId=(Products and collections).combinesWithsays whether the offer may stack with other product, order and delivery offers.eligibilitylimits who and where:audienceiseveryone,first_orderorreturning;channelsare amongstore,landing,shopify,dashboardandapps, andwilayasanddeliveryMethods(home,desk) narrow it further. An empty list means no limit.totalUsageLimitandperBuyerLimitare the limits the merchant set (null= none).appOwnedis 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/createdis sent when an offer is created, withdata.offer(the offer above). A new offer is always a draft: you hearoffers/updated, withchangednamingstatusandpublishedAt, when it goes live.offers/updatedis sent when the offer reads differently: publishing, pausing, archiving, its dates, its rules, its targets or its codes.changednames the top-level fields that differ (status,pausedAt,endsAt,quantity,targets,codeCount, …), anddata.offeris the offer as it is now. A change that alters nothing an app can read sends nothing.offers/deletedis sent when an offer is deleted, withdata.offer = { "id": … }and nothing else. Only an offer that was never redeemed can be deleted: a redeemed one is archived, which isoffers/updatedwithstatus: "ended"andarchivedAtset.
webhook-id, and
when two bodies of one offer arrive out of order, keep the one with the later
updatedAt.