Skip to main content

Coupon codes

A coupon program is a recipe you set up once - for example "Welcome 10%: 10% off, minimum spend 50, valid 14 days from the moment it is sent, one use". Whenever a campaign or a journey needs a code for a person, Joryio issues one from the program: it creates a unique, single-use code in your connected WooCommerce, Shopify, Magento or PrestaShop store and puts it in the message. When the person orders with it, the order sync recognizes the code, marks it redeemed and credits the revenue to the message that issued it.

Codes are free - issuing them is not billed.

Create a program​

Open E-Commerce → Coupons and select New program. Select a program's row to open its report.

FieldWhat it does
NameA label for your team, for example "Welcome 10%".
SlugThe handle you use in messages: {{ coupons.<slug>.code }}. Lowercase letters, digits and underscores; filled in from the name.
StoreThe connected WooCommerce, Shopify, Magento or PrestaShop store the codes are created in. Or No store - Joryio generates and tracks the codes, and you apply them in your own checkout, POS or app. Redemptions are then recognized from orders sent through the Orders API.

A new program starts as a draft. Activate it with Activate on the program's page (or in the row's menu in the list); Pause stops it again. Only an active program issues codes; paused and archived programs issue nothing, and the message then follows the program's fallback.

Discount settings​

SettingOptions
Discount typePercent off, fixed amount off the cart, fixed amount off each product (not on PrestaShop), or free shipping.
AmountThe percent or the amount in the store currency (not used for free shipping).
Minimum spendOptional cart subtotal the order must reach.
Minimum quantityOptional, Shopify only.
Applies toThe whole cart, or chosen products (WooCommerce, Shopify, PrestaShop), categories (WooCommerce, Magento, PrestaShop) or collections (Shopify). Choosing products is not available on Magento in this release.
Exclude sale itemsWooCommerce only.
Uses per codeDefault 1 (single use).
Once per customerDefault on: one customer cannot use codes from this program twice.
ExpiryNone, a fixed date, or N hours / days after the code is issued - optionally rounded to the end of that day in the workspace timezone.
Can be combinedDefault off: the code cannot be used together with other discounts.
Code formatLength 6-20, character set (letters and digits without look-alikes such as 0, O, 1, I and L - the default; letters only; digits only), prefix (WELCOME-), suffix and a separator every N characters.
Reuse the person's open codeDefault on: a person who still has an open (not redeemed, not expired) code from this program gets that code again instead of a new one.
Lock to the personWooCommerce, Magento and PrestaShop - see Lock to the person.

A percent cap ("15%, at most 50") is not offered: none of the stores supports it natively.

Lock to the person​

By default every code is unique and single-use, so a forwarded code costs at most one discount. On WooCommerce, Magento and PrestaShop you can also lock each code to the person it was sent to: the store then refuses it for anyone else.

  • WooCommerce uses the coupon's own email restriction.
  • Magento needs the Joryio Connect extension 1.2.0 or later, which checks the code against the person at checkout. A Magento store without it gets unlocked single-use codes, and a per-code expiry (N hours / days after issue) is enforced by Joryio deleting the code within the hour after it expires - the same as on Shopify.
  • PrestaShop (Joryio module 1.1.0 or later) locks the code to the person's customer account in the shop (the cart rule's customer): they sign in to use it. A person without a registered account in the shop, or with two accounts on the same email, gets no code - the issue fails and the program's fallback applies.
  • Shopify: the switch is not shown. Shopify decides who can use a discount for the whole discount, not per code, so every Shopify code is unique and single-use, with one use per customer.

A personal code for a guest​

A guest who opens the apply link of a locked code gets it as follows:

  • WooCommerce (Joryio plugin 1.6.29 or later): the code waits in the shopper's session and is applied as soon as their email is known and is the one it was sent to - at once for a signed-in customer, and for a guest when they enter their email at checkout (block and classic checkout), at the latest when the order is placed. Until then the cart and checkout say "Your code ... is personal: it will be applied at checkout when you enter the email address it was sent to." Another email gets "The code ... belongs to another customer, so it was not applied" - it never says whose - and no discount; correcting the email applies it. WooCommerce checks the email itself when the code is applied and on the order, so a wrong email never gets the discount. Older plugins: WooCommerce refused the code until the email was known ("Please enter a valid email at checkout to use coupon code ...") and never tried it again.
  • Magento (extension 1.2.0 or later): the code is applied to the guest's cart and shows its discount; the extension checks it against the email when the order is placed and refuses another email ("This coupon code was sent to another customer").
  • PrestaShop: the code belongs to a customer account, so a guest is asked to sign in first and returns to the cart with the code.
  • Shopify: codes are not locked, so a guest uses the code like anyone else.

When no code can be issued​

If the store cannot create a code (it is down, a permission is missing, the program is paused), the program's fallback decides what the message does:

FallbackWhat happens
Skip the message for this person (default)The message is not sent to that person and is recorded as skipped with the reason coupon_unavailable.
Send a shared fallback codeThe message goes out with the shared code you enter (for example SORRY10).
Send without the couponThe message goes out and {{ coupons.<slug>.code }} is empty - wrap the coupon part in {% if coupons.<slug>.code != '' %} to hide it.

A message sent through the transactional API is refused instead: with Skip, the call answers 422 with code: coupon_unavailable and the reason, and nothing is sent. In-app messages and WhatsApp campaign messages cannot be withheld yet, so for them Skip works like Send without the coupon: the message goes out with an empty code. Wrap the coupon part in {% if coupons.<slug>.code != '' %} there.

Code sources​

Each program has a code source - where its codes come from. Choose it when you create the program; it cannot change once the program has codes.

SourceWhat happensBest for
Personal (default)Joryio creates one unique code per person in your store at the moment the message needs it (or generates it alone when the program has no store).Journeys, triggered and smaller campaigns - welcome, birthday, win-back.
Code bankJoryio creates many codes in your store ahead of time and hands them out one per person.Large campaigns (tens of thousands of people), where creating codes one by one at send time would be too slow.
Uploaded listYou upload codes made elsewhere - your POS, a partner, a custom store or app, another tool - and Joryio hands them out one per person. Joryio never creates them in a store.Codes Joryio cannot create, and stores Joryio does not connect to.
Shared codeOne code for everyone (SUMMER10), created in your store or just registered. Every send is still recorded per person.Broadcast promotions.

Whatever the source, the message uses the same {{ coupons.<slug>.code }} variable, a person never gets the same bank or list code twice, and the same send always shows the same code.

Uploading codes for a custom store or app​

An Uploaded list program works with no store connected - for example codes your till system or your own app accepts - and also with a connected store when the codes already exist there (Joryio then does not create them again).

  1. Create a program with the source Uploaded list. The store is optional; the discount settings only describe the value the message shows ({{ coupons.<slug>.value }}).
  2. On the program, select Upload codes and choose a CSV file or paste the codes.
  3. Check the preview - Joryio validates the file first and shows how many codes will be added and every rejected row with the reason. Nothing is stored until you confirm.
  4. Confirm. You can upload more codes to the same program at any time; earlier codes stay.

The file:

ColumnRequiredFormat
codeYes3-64 characters: letters, digits and - _ . # / +
expires_atNoYYYY-MM-DD (the end of that day, UTC) or an ISO date-time. The code is not handed out after it.
valueNoWhat {{ coupons.<slug>.value }} shows for this code, for example 10% or Free coffee - up to 100 characters.

The header row is optional (without it the columns are taken in this order), and commas, semicolons and tabs all work. A file holds up to 100,000 rows and 10 MB; split larger lists into several uploads. Rows are rejected when the code is missing or not valid, the date is not a date or has passed, the value is too long, the code appears earlier in the file, or the code is already in this or another program of the workspace (codes are matched to orders by the code itself, so each code can belong to one program only). Codes are compared without regard to case.

Each person gets one unused code, and a code is never given to two people - even when many messages go out at the same moment. When an order with an uploaded code reaches Joryio - through a store sync or the Orders API with the code in the order's coupons - the code is marked redeemed and the revenue is credited to the message, exactly as for codes Joryio created.

Code banks​

A Code bank program needs a connected store. Joryio creates the codes in the store before anyone receives them:

StoreHow the codes are created
ShopifyAdded to the program's one discount, 250 per call.
WooCommerceOne coupon per code, 100 per call.
MagentoGenerated under the program's cart price rule. Magento chooses the code strings itself from the program's code format (length, character set, prefix, suffix, separator).
PrestaShopOne cart rule per code, one call each.
  • Generate codes on the program creates codes now; it runs in the background, and the program page shows the progress and the last result.
  • Codes to keep ready keeps the bank at that size: every 15 minutes Joryio tops it up when it falls below.
  • Before a campaign sends, Joryio tops up every bank the content uses to cover the audience, so the send never waits on the store. Anything still missing when the send starts follows the program's fallback.
  • Lock to the person (WooCommerce, Magento and PrestaShop) binds a bank code to the person when it is handed out.
  • Changing the discount of a bank that holds codes retires its unused codes (they are deleted from the store) and new codes are made under the new rules.

Shared codes​

A Shared code program gives everyone the same code. Enter the code, and choose whether Joryio creates it in your store or only registers a code that already exists there (or in your POS). A shared code cannot be locked to the person, and a code created in the store should allow more than one use.

Every send is still recorded per person, so the report shows who received the code from which message. An order with a shared code is credited to a message only when the order can be matched to the contact who received it - their most recent send of the code counts. Orders from guests that Joryio cannot match to a contact are not attributed, because the code alone cannot say which message they came from.

Low-stock alerts​

Code banks and uploaded lists show the codes left on the program list and the program page. Set the alert on the program:

  • Fewer than N codes left (default 500), and / or
  • Fewer than N% of all the codes loaded left.

When an active program falls below either, the alert appears in the notifications bell and on the program page; it resolves by itself when you add codes. An empty program raises a critical alert.

Launch Review checks the stock too:

  • A campaign whose audience is larger than the codes left in a list or bank is blocked - upload or generate more codes first.
  • A journey is blocked only when the list or bank is empty (a journey has no fixed audience).
  • A send that would leave the stock below the alert gets a warning.

When codes run out during a send, each remaining person gets the program's fallback: the message is skipped, sent with the shared fallback code, or sent without the coupon.

Use a code in a message​

Write the program's variable in any message - email, SMS, push, web push, in-app, WhatsApp or Viber:

VariableValue
{{ coupons.welcome10.code }}The person's code, for example WELCOME-7KQ4M2XH9P.
{{ coupons.welcome10.expires_at }}When it expires, as a date and time people can read: in the contact's language (their locale attribute, else language, else the workspace's, else English) and the workspace time zone. For example November 3, 2026 at 6:30 PM, or 3 בנובמבר 2026 בשעה 18:30 for a Hebrew contact. Empty when the code never expires.
{{ coupons.welcome10.expires_at_iso }}The same moment as an ISO timestamp (2026-11-03T16:30:55.147Z), empty when the code never expires. Use it to pick your own format with a date filter, which also formats in the workspace time zone: {{ coupons.welcome10.expires_at_iso | date: "%d/%m/%Y" }}. A date filter already written on expires_at keeps working.
{{ coupons.welcome10.value }}The discount in words: 10%, 5 EUR, Free shipping.
{{ coupons.welcome10.apply_url }}A link that opens the store with the code already applied (empty when the store has none).
{{ coupons.welcome10.name }}The program name.

In the email editor, drag the Coupon block into the email and pick the program: it shows the code, the expiry date and a button to the apply link, and hides itself when no code was issued.

Only codes a person actually sees are issued. A code inside an {% if %} branch the person does not get is never created. The same send always shows the same code: a retry, a resend, a switch between A/B variants and an SMS fallback all carry one code.

Previews and test sends​

The preview always shows a sample code such as WELCOME-PREVIEW - with the store's apply link for it in apply_url - and never creates one. A test send shows the sample too, unless you tick Issue a real coupon code (single use, expires in 24 hours) - the box appears only when the content uses coupons: Joryio then creates a real code - single use, valid 24 hours, marked as a test - so you can try it at checkout. This works on a draft program too. If the code cannot be created - the store refuses it, the program is archived or has no codes left, or the program locks codes to the person and the recipient has no email address - the test is not sent and the dialog shows why. Test codes are not counted in the program's report or on the contact's Coupons tab.

In a journey​

Add an Issue coupon step (in the palette's data group) and choose the program. It issues the code once and keeps it with the person: every later message in the journey that uses {{ coupons.<slug>.code }} shows the same code - the email and the SMS reminder three days later included. The step has two exits:

  • Issued - the person has a code.
  • Could not issue - the store could not create one; route these people to a message without a coupon, or end the journey.

A message that uses the variable without an Issue coupon step before it issues the code itself, and later messages in the same journey show that code. See Canvas nodes.

Redemptions and revenue​

When an order arrives from WooCommerce, Shopify, Magento or PrestaShop (or through the Orders API) with a code a program issued, Joryio marks the code redeemed, records the order and its revenue, and emits the Coupon Redeemed event. The first order with the code counts. If someone other than the person it was issued to uses it, the report shows it as "redeemed by someone else".

Coupon attribution. An order that used a code a message issued is credited to that message - ahead of clicks and opens. This is on for every workspace. Because the code is the strongest evidence there is, revenue can move between campaigns in existing reports once codes are in use.

Expiry. When a code expires, Joryio marks it expired, emits Coupon Expired and deletes the code from the store automatically, so your store's discount list does not fill with dead codes. The history stays in Joryio.

Program report​

Each program shows codes issued, redeemed, redemption rate, revenue from orders that used a code, discount given, expired and failed codes, codes redeemed by someone else, and a breakdown by campaign and journey. Test codes are not counted.

A contact's profile lists their codes on the Coupons tab (needs Users (read)): program, code, status (open, redeemed, expired), the message that issued it and the dates.

Events​

EventWhenProperties
Coupon IssuedA code was issued to the person.program, program_id, campaign_id, canvas_id, node_id, source, expires_at
Coupon RedeemedAn order used the code.program, program_id, campaign_id, canvas_id, order_id, revenue, redeemed_by_someone_else
Coupon ExpiredThe code expired unused.program, program_id, campaign_id, canvas_id, expires_at

The code itself is never on an event: a code is worth money to whoever holds it.

Store setup​

StoreWhat to do once
ShopifyOpen Integrations → Shopify and select Approve new permissions. Coupons need Shopify's discount permission (write_discounts); stores connected before it existed approve it once.
WooCommerceNothing for issuing codes - the plugin's connection already allows it. The apply link (apply_url) needs the Joryio plugin 1.4.0 or later.
MagentoUpgrade the Joryio Connect extension to 1.2.0 or later. The upgrade grants the integration the cart price rule permission (Magento_SalesRule::quote) that creating codes needs, and adds locking to the person.
PrestaShopInstall or upgrade the Joryio module to 1.1.0 or later. The upgrade lets the module's Webservice key manage cart rules (and nothing else), applies a product or category choice, and adds the apply link. See PrestaShop coupon programs.

On Shopify each code takes one store call, and a standard Shopify plan allows about 10 a second, so a campaign to a very large audience can take a while to prepare its codes. For a large campaign, use a code bank: its codes are created in the store before the send.

In the Shopify admin, all of a program's codes sit under one discount titled Joryio - <program name> (<slug> v<n>). Changing the rules of a live program creates a new discount (the next version) for new codes; codes already issued stay on the old one. Shopify keeps one expiry date per discount, so a per-code expiry (N hours / days after issue) is enforced by Joryio deleting the code within the hour after it expires.

Permissions and privacy​

  • Campaigns (write) creates programs. Changing a live program - one that has issued codes - needs Settings (write); codes already issued keep the rules they were created with.
  • Seeing the codes on a contact needs Users (read).
  • Demo (sandbox) workspaces never create real coupons in a real store.
  • Erasing a person (GDPR) deletes their codes in Joryio and removes their open codes from the store.