Promotion Publishing Rules
Last updated: September 29, 2026
A promotion reaches shoppers only when its visibility, channels and dates line up, and each channel shows one public promotion at a time. This guide explains how the Abra API applies these rules and gives a safe order of calls for launching a promotion. For endpoint and field details, see Promotions API.
Beta. The Abra API is in beta, so its behavior may change. If something doesn't work as described, contact us at help@abrapromotions.com.
Promotions and discounts
A promotion is the part shoppers see: it sets who sees it, where and when it runs, and what appears on your storefront. Its discounts are the Shopify discount codes or automatic discounts that reduce the price at checkout. A promotion can have several.
Attach discounts in either of two ways:
Create the discount with
promotionIdset to the promotion'sid. See Create a discount.Send Shopify discount GIDs in the promotion's
discountsarray, each asshopifyDiscountId. On update, this array replaces the promotion's current discounts.
A promotion shows nothing on the storefront until it has at least one discount.
Visibility
visibility decides who sees a promotion. It defaults to PRIVATE.
Value | Who sees it |
|---|---|
| Only visitors who open its private link, which is any store URL with |
| Every visitor on the channels in |
| Logged-in customers with one of the listed Shopify customer tags. Separate tags with commas, for example |
The one-public-promotion rule below applies only to PUBLIC, so PRIVATE and TAG- promotions can run alongside a public one. Use different tags for different TAG- promotions, because the storefront keeps one promotion per tag. For the merchant side of these settings, see Run Private VIP Promotions and Customer Account Visibility.
Channels
publicTargets lists the channels a PUBLIC promotion runs on:
onlineStore: your Shopify online storetapcart: your Tapcart mobile app, when the Tapcart integration is enabled in Abrapos: Shopify POS
If publicTargets is empty or missing, a PUBLIC promotion runs on onlineStore and tapcart, but not on pos. PRIVATE and TAG- promotions ignore publicTargets.
One public promotion per channel
Each channel shows one live PUBLIC promotion at a time. A PUBLIC promotion is live from its startsAt until its endsAt. Promotions on different channels don't conflict, so one can run on onlineStore and tapcart while another runs on pos.
Abra checks this during the request, whenever a create or update would leave a PUBLIC promotion live. Two PUBLIC promotions conflict when they share at least one channel, and an empty publicTargets counts as onlineStore and tapcart. A PUBLIC promotion that starts later doesn't conflict with anything until it starts.
A live PUBLIC promotion holds its channels even when it has no discounts and shows nothing.
What happens on a conflict
A conflicting request is not rejected. What happens depends on how you publish:
On create (POST) with
forceReplacePublicPromotion: true, Abra expires the PUBLIC promotions that are live on any of its channels, and yours goes live. The replaced promotions become EXPIRED and PRIVATE with no channels. Their discounts keep running.On create without it (the default), your promotion is saved as EXPIRED and PRIVATE:
publicTargetsis cleared,useDiscountDatesis turned off andendsAtis set to about a minute before the request. The live promotion is unchanged.On update (PUT), you get the same result as a create without the flag, because PUT ignores
forceReplacePublicPromotion. This includes a promotion that was already live, for example when you add a channel that another live promotion uses.If the promotion starts in the future, nothing is checked when you save it. At its start time, Abra activates it in the background and expires any PUBLIC promotion that is live on its channels, whether or not you set
forceReplacePublicPromotion.
When your promotion loses, POST still returns 201 and PUT still returns 200. The response body, and the promotions/create or promotions/update webhook for that request, can still show it as PUBLIC, and after a PUT also as ACTIVE. Read the promotion back with GET to see what was saved.
Start and end dates
Abra sets a promotion's status from its dates when you save it: SCHEDULED if startsAt is in the future, ACTIVE once it has started, and EXPIRED after endsAt. If you send status, it overrides this, so leave it out unless you need it. A promotion without an endsAt runs until you end it. Shortly after endsAt, Abra marks the promotion EXPIRED, makes it PRIVATE and clears publicTargets.
useDiscountDates, which defaults to true, lets the discounts set the dates:
On create, while it's
true, Abra ignores anystartsAtandendsAtyou send. The promotion starts with the earliest of its discounts that hasn't ended and ends with the last one to end. It has no end date if any discount has none. With no discounts, it starts right away and has no end date.On update, it only applies when the request includes a non-empty
discountsarray, and Abra then recalculates the dates from them. Otherwise Abra uses thestartsAtandendsAtyou send.To set your own dates, send
useDiscountDates: falsewith astartsAt. A promotion without a start date is saved as EXPIRED.
Creating a discount with promotionId, or updating a discount that belongs to a promotion, also sets the promotion's startsAt and endsAt to that discount's dates, even when useDiscountDates is false.
Deactivate or delete
Deactivate a promotion to end it now. It stays in Abra, EXPIRED and PRIVATE with no channels, and you can publish it again later. The response is
200with the promotion.Delete a promotion to remove it and its storefront data for good. The response is
204with no body.
Either way, the promotion's discounts keep running in Shopify. To end them too, set deactivateAssociatedDiscounts to true in the request body. This is best effort: the promotion is still deactivated or deleted if a discount can't be.
To publish a deactivated or expired promotion again, send a new startsAt and endsAt (use null for no end date) together with visibility and publicTargets in one PUT. Setting only visibility leaves it EXPIRED and PRIVATE.
Bulk update promotions applies activate, deactivate or delete to a list of IDs. For deactivate and delete, you can also send deactivateAssociatedDiscounts. activate starts the promotions now with no end date and leaves their visibility as it is. The call returns 200 even if some IDs fail, so read the promotions back afterwards.
Safe publishing sequence
To launch a public promotion without surprises:
Create the promotion as PRIVATE, the default, so only people with its link can see it while you set it up. Keep the
idfrom the response; the examples below use it as$PROMOTION_ID. They also use$ABRA_API_BASEand$ABRA_API_TOKENfrom the Abra API Overview.curl -X POST "$ABRA_API_BASE/v1/partner/promotions" \ -H "Authorization: Bearer $ABRA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"title": "Summer Sale 2026", "visibility": "PRIVATE"}'Attach its discounts, with either method from Promotions and discounts above.
Check that the channels you want are free. The call below lists running public promotions with their
publicTargets. If one uses a channel you need, deactivate it or set it to PRIVATE first, because PUT won't replace it. A PUBLIC promotion with status SCHEDULED also takes over its channels when it starts, so check for those withstatus=SCHEDULEDtoo.curl "$ABRA_API_BASE/v1/partner/promotions?visibility=PUBLIC&status=ACTIVE" \ -H "Authorization: Bearer $ABRA_API_TOKEN"Publish it. If the promotion has already started, it goes live during this request. If it starts later, it stays SCHEDULED and takes over its channels at its start time.
curl -X PUT "$ABRA_API_BASE/v1/partner/promotions/$PROMOTION_ID" \ -H "Authorization: Bearer $ABRA_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"visibility": "PUBLIC", "publicTargets": ["onlineStore"]}'Read it back with
GET /v1/partner/promotions/{id}. It's published whenvisibilityis PUBLIC,statusis ACTIVE (or SCHEDULED for a later start) andpublicTargetslists your channels. If it's EXPIRED and PRIVATE, it lost a conflict or its dates have passed. Free the channel if needed, then publish it again with new dates.
To replace the live promotion in a single call instead, create the new promotion with visibility set to PUBLIC, its discounts, and forceReplacePublicPromotion: true.