write_offers an app runs offers of its own: an influencer platform that
mints a code per influencer, a spin-to-win wheel, a referral program, a
flash-sale scheduler, a “mix and match” the merchant can create from your
app. It creates an offer, adds codes to it, publishes, pauses and archives it,
and reads who used it. Offers is the read side and the
description of an offer; read it first.
write_offers implies read_offers, so you read every offer of the merchant
and the offers webhooks. It is a normal scope, and 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. An online token
also needs the person’s own permission to create, change and delete offers.
No particular plan is needed, but the plan’s limit on live offers (a Starter
merchant’s three) applies to your offers exactly as to the merchant’s: publish
and resume answer 403 PLAN_LIMIT_REACHED past it. And an offer prices an
order only on a business where Tassyir’s order pricing is on, which is rolled
out business by business: until then an offer you create is stored, listed and
published, but takes nothing off an order.
You write only the offers you own
An offer your app creates is owned by your installation; there is no field to set that. The rule:- You change, and read the codes and redemptions of, only the offers your own
installation created. A write on the merchant’s offer, or on another
app’s, is 403
OFFER_NOT_OWNED, and so is reading its codes or redemptions. Thefixsays what to do: create your own offer and change that one. - An offer the token cannot see (an id that does not exist or is malformed,
another business’s, a store outside the installation’s stores) is 404
NOT_FOUNDon every route, writes included. - Reads stay as they are. You list and read every offer your token can
see.
appOwnedis true for any app’s offer, so it does not say “yours”: keep the ids of the offers you create, and a 403 tells you the rest. - The merchant keeps control. The dashboard writes every offer, an app’s
included: pause it, archive it, add codes. You hear about it with
offers/updated; that is not an error of yours. - Uninstalling leaves your offers as they are. A new installation of the same app does not own them: the merchant manages them in the dashboard.
- A store-limited installation creates offers in its own stores only: a
storeIdoutside them is HTTP 460STORE_SCOPE_FORBIDDEN(Admin API conventions).
Create an offer
An offer is created as a draft, then published.config is the offer’s
rule in the shape the Offers page stores it, which is not the shape
GET /offers answers (money is a number of whole dinars, a quantity tier
carries price). Every field is checked by the same rules as the dashboard.
config per kind (money is whole dinars; an unknown key is OFFER_INVALID
with UNKNOWN_FIELD):
You read an offer back in the block of its kind (
quantity, freeDelivery,
productDiscount, orderDiscount, bxgy, addon or, for an app’s own kind,
appRule), in the shape Offers describes.
A draft is saved as long as everything present is well formed; publish
also checks completeness (a title, the kind’s required settings, at least one
target where the kind needs one, at least one enabled code for a code
offer, an endsAt not in the past). A refusal is 400 OFFER_INVALID with
errors, a list of { field, code } where field is a dotted path
(config.tiers.1.pieces) and code says why (REQUIRED, INVALID_VALUE,
UNKNOWN_FIELD, TOO_LARGE, NOT_WHOLE_DINARS, NOT_ASCENDING,
ALL_IS_EXCLUSIVE, …). Act on code, show field to your user.
Influencer codes, in bulk
The classic: one offer, one code per influencer, each with its own usage limit. Codes are unique across the business, whatever the case (SARA10
and sara10 are one) and whether disabled or not, are 3 to 30 letters,
digits, - or _, and are never reused: disabling keeps a code’s spelling
and its history.
results has an outcome:
POST /offers/{id}/codes/bulk takes 1 to 250 codes (more is 400
VALIDATION_FAILED and nothing is written) and answers 200 whatever the mix.
A single POST /offers/{id}/codes answers 201, or 409 OFFER_CODE_TAKEN,
OFFER_NOT_CODE_METHOD (an automatic offer has no codes) or 400
OFFER_CODE_INVALID. Disable and enable are idempotent; GET /offers/{id}/codes lists the codes newest first, disabled ones included:
uses counts the redemptions that still count against usageLimit (held or
final): a cancelled order gives its use back. Codes appear nowhere else: the
offer, the webhooks and an order show a count, or nothing.
Publish, pause, resume, delete
publish, pause and resume are idempotent: asking for the state the
offer is already in changes nothing and sends no event. Their body is
optional: send none, or { expectedVersion } (below).
DELETE answers which it did, so you do not guess:
PATCH /offers/{id} changes the fields you send, from the table above
(kind and method may be echoed back but never change: 400
OFFER_KIND_IMMUTABLE). targets and config, when sent, replace the
whole set; an empty body is 400. A live offer must stay publishable. Every
write on an offer can carry expectedVersion, the dashboard’s compare and
set: a stale one is 409 OFFER_VERSION_CONFLICT with currentVersion. Without
it the change applies to the offer as it is when the request starts.
Idempotency-Key
(Admin API conventions); the typed client
sends one for you.
Redemptions
GET /offers/{id}/redemptions is each use of an offer you own on an order,
newest first, in pages. state filters it: held (the order is not with a
carrier yet), final (handed over) or released (the order was cancelled, or
the buyer banned).
amount is what the offer took off that order, a decimal string. Never a
buyer: no name, phone or address. orderId is the one link: read the order
with GET /orders/{id} (read_orders, and read_customer_contact for the
buyer’s contact fields, as for any order). An installation that owns the offer
but cannot read orders sees ids it cannot open. codeId and code are null
for an automatic offer, and code is null once the code was removed.
Applying a code to an order
An app does not apply an offer to an order: orders get their offers from Tassyir’s pricing. When your app creates an order for a buyer who typed a code, send it:codes takes one code at most (case and surrounding spaces do not
matter). A code that does not apply never fails the order: the order is created
without it and rejectedCodes names the code with the reason (code_unknown,
code_expired, code_used_up, not_eligible_audience, beaten_by, …). The
order’s discounts[] entry carries the code and source: "code"
(Order money). A usage limit that runs out between
the pricing and the write is 409 OFFER_LIMIT_REACHED and nothing is created.
Webhooks
You hear your own writes, like any installation: the topics are business-wide, so the app that created an offer receivesoffers/created for
it (Offers). A new offer is a draft, so offers/updated
with changed naming status and publishedAt is when it goes live; adding
or disabling a code sends offers/updated with changed: ["codeCount"].
Deduplicate on webhook-id, and when two bodies of one offer arrive out of
order keep the one with the later updatedAt.
Your own offer types
A native offer is one of the kinds above. An app can add kinds of its own that the merchant creates from your app: “mix and match”, a spin-to-win prize, a loyalty tier. You declare each type in the manifest, and you write its offers withkind: "app". Pricing stays in Tassyir: an app-owned offer is
a declarative rule, JSON the pricing engine evaluates, so no code of yours
runs at checkout, it costs nothing at checkout and it can never fail an order.
Declare the types in tassyir.app.toml (at most 10; declaring any needs the
write_offers scope):
The merchant sees your types in the create dialog of the Offers page, “From
your apps”, for every active installation of your app that holds
write_offers (directly or implied) and may act on that store. Choosing one
opens your embedded page at the type’s path (Embedded pages),
which is where the offer is set up: your page creates the offer with the Admin
API, and later changes it.
typeHandle must be one of the [[offers.types]] of the version your
installation runs, else 400 OFFER_INVALID with config.typeHandle: NOT_DECLARED. Every product, variant and collection the rule names must
exist in the offer’s store (400 OFFER_TARGET_NOT_FOUND or
OFFER_TARGET_OUTSIDE_STORE).
- The merchant manages the offer’s life, you manage its settings. On the
Offers page an app-owned offer can be published, paused, resumed and
archived (and its codes managed), but editing or duplicating it there
answers 409
OFFER_APP_OWNED: it is changed in your app, at the type’spath. Your ownPATCHcalls on your own offers are not affected. - It prices only while your installation is active and your app is not suspended. Uninstalling, or a suspension, stops it pricing orders at once; the offer is kept, and reinstalling or resuming prices it again.
- An offer of a type is owned by the installation that created it, like any other offer of yours.
The rule
A rule is{ class, when?, then }, validated on every write (400
OFFER_INVALID, with a coded issue per problem) and read again on every
price: a stored rule that no longer parses prices nothing, never fails an
order.
when is absent (always), a leaf, or { "all": [...] } / { "any": [...] } of 1 to 10 conditions, nested at most 3 deep (TOO_DEEP). A leaf is
an object with at least one of these keys (an empty one is EMPTY):
The lines a satisfied leaf picked are the lines the product actions apply to
(every priceable line when no leaf picked any).
then is 1 to 5 actions, each key once (DUPLICATE), at least one that
prices (NO_ACTION), all of the rule’s class (ACTION_CLASS_MISMATCH):
A gift is never added by Tassyir: when the rule is satisfied and the gift
is missing, the cart answers with a suggestion, the buyer’s form adds the gift
as a visible, removable line, and the engine then discounts it.
percent is above 0 and at most 100; dinars are whole numbers. The issue
codes you can get in errors[].code, besides those of every offer
(REQUIRED, INVALID_TYPE, INVALID_VALUE, TOO_LARGE, …):
Errors these routes add
Beyond the codes every route shares (Errors):OFFER_APP_OWNED (409) is not one of these: it is what the merchant’s
dashboard answers when someone tries to edit or duplicate an app-owned
offer, as described above. Your own calls never get it.