Help · Analytics
Connect Shopify Flow
Some of what happens in your store never reaches us on its own. Store credit, product reviews and loyalty points are recorded by apps of yours, and Shopify sends no notice of any of them to anybody else. Nothing about that is a fault in those apps — there is simply no message for us to listen for.
Shopify Flow can send us one. It has an action called Send HTTP request, which posts whatever you type into it to an address you choose. Point it at us and the thing that just happened becomes an event in this store's record — countable on the Metrics page, usable as a rule in the segment builder, and available as the start of an automation.
Shopify Flow's Send HTTP request action needs the Grow, Advanced or Plus plan. On the Basic plan the action is not offered, and nothing on this page will work. That is Shopify's limit, not ours.
What you need before you start
A key for your store. Every request carries one, and it is what tells us whose store the event belongs to. There is no screen in this app that makes one yet — ask us and we will send you one. It looks like ep_ followed by a long run of letters and numbers.
Treat it the way you would treat a password. Anyone holding it can write events into your store's record. It cannot read anything, it cannot send email, and it cannot change a campaign — but it can add to your history, so keep it out of shared documents.
Shopify Flow keeps keys for you. In the Send HTTP request action, the header value box has an Add secret option. Put the key there rather than typing it in plainly, and it stops being visible to anyone who opens the workflow afterward.
The parts every recipe shares
Three of the four boxes are the same every time.
| Box | What to put in it |
|---|---|
| HTTP method | POST |
| URL | https://app.carpemessaging.com/api/v1/events |
| Header | Name Authorization, value: the word Bearer, a space, then your key |
| Header | Name Content-Type, value application/json |
Only the Body changes, and that is what the six recipes below are.
Everything in double braces is a variable, filled in by Shopify Flow when the workflow runs.
You cannot type one. Shopify Flow's Body box only accepts a variable put there by its own Add variable control — type the braces yourself and it refuses them as invalid. So paste the body first, then put the cursor where each variable belongs, delete the placeholder, and choose the matching value from Add variable. The braces below show you where each one goes and what it should be.
The quotation marks are ours, not the variable's. Every text value in the bodies below sits inside a pair of quotation marks that is already typed; leave them alone and put the variable between them. The four values that are numbers — amount, balance, points and days remaining — carry no quotation marks, because a number in quotation marks stops being a number and rules that compare it stop working.
Your app's names will differ from the ones shown. The list Shopify Flow offers beside the Body box is the truth about what your trigger carries. If a value shown below is not in that list, leave the property out rather than guessing at it.
Recipes 1 to 3 — store credit
Start from your store-credit app's own triggers. Shopify has no trigger of its own for store credit: a credit does not reach us, and it does not reach Shopify Flow either, so the trigger has to come from the app that issued the credit. Store-credit apps usually publish several — one for a credit being given, one for a credit about to run out, one for a balance changing — and each is worth its own workflow.
When someone is given credit. Start from the trigger named for rewarding or crediting:
{"events":[{"name":"Store Credit Rewarded","email":"{{ customer.defaultEmailAddress.emailAddress }}","properties":{"amount":{{ trigger.amount }},"currency":"{{ trigger.currency }}","expires_at":"{{ trigger.expiresAt }}"}}]}
When a campaign gives credit. Some store-credit apps reward people for entering or leaving a customer segment through a separate trigger, usually named for a campaign. It is the same fact as a reward with a campaign attached, so it records the same event with two more properties. An automation that starts on "Store Credit Rewarded" then catches both kinds, and one filtered on the campaign id catches only that campaign's:
{"events":[{"name":"Store Credit Rewarded","email":"{{ customer.defaultEmailAddress.emailAddress }}","properties":{"amount":{{ trigger.amount }},"currency":"{{ trigger.currency }}","expires_at":"{{ trigger.expiresAt }}","campaign_id":"{{ trigger.campaignId }}","campaign_name":"{{ trigger.campaignName }}"}}]}
When someone's credit is about to run out. Start from the trigger named for an expiration threshold — these usually fire a set number of days before the date, and that number is worth recording, because an automation that says "your credit runs out in three days" needs it:
{"events":[{"name":"Store Credit Expiring","email":"{{ customer.defaultEmailAddress.emailAddress }}","properties":{"amount":{{ trigger.amount }},"currency":"{{ trigger.currency }}","expires_at":"{{ trigger.expiresAt }}","days_remaining":{{ trigger.remainingDays }}}}]}
When a balance changes. Start from the trigger named for a balance update:
{"events":[{"name":"Store Credit Balance Updated","email":"{{ customer.defaultEmailAddress.emailAddress }}","properties":{"balance":{{ trigger.balance }},"currency":"{{ trigger.currencyCode }}"}}]}
One workflow per trigger, and one body per workflow. Build each of the four above as its own workflow with the same URL, the same two headers and the same key; only the trigger and the body differ. Each placeholder is replaced by the value of the same name in Add variable — Amount, Currency, Expires at, Campaign ID, Campaign name, Remaining days before expiration, Balance, Currency code — and the person's email is always Customer → defaultEmailAddress → emailAddress. Amount, balance and days remaining go between no quotation marks; everything else keeps them. A trigger for credit that has been scheduled rather than given is a promise, not an event, and has no recipe.
This works on a live store. The balance recipe above was built exactly this way and its event arrived with the balance as a number and the person named — the other three differ from it only in the trigger and the property names.
Check the email value first, before anything else. Some store-credit triggers list only the money — amount, currency, expiry — and offer the person as a customer rather than as an address. When that is what yours does, Add variable offers a Customer group, and the value to choose is the same one every recipe here uses: Customer → defaultEmailAddress → emailAddress. If the trigger offers no Customer group at all, it cannot name the person, and this recipe cannot work with it.
Recipe 4 — reviews
Start from your review app's own trigger, which is usually named for a new review arriving.
Body:
{"events":[{"name":"Reviewed a Product","email":"{{ customer.defaultEmailAddress.emailAddress }}","properties":{"rating":{{ trigger.rating }},"product_title":"{{ trigger.productTitle }}","product_url":"{{ trigger.productUrl }}","order_id":"{{ trigger.orderId }}"}}]}
The event is called Reviewed a Product whichever review app you use. That is the point of writing it this way: a rule about people who left five stars goes on working if you change review apps, and your history stays one line rather than two.
Some review triggers carry no email address — they name the order instead, and leave you to look the address up. When that is what yours does, add a Send Admin API request step ahead of the Send HTTP request one to fetch the order's email, and use that step's own email value in its place. If you would rather not, the recipe is still worth having for reviews that do carry an address.
Recipes 5 and 6 — loyalty points
Start from your loyalty app's own trigger. These differ more than the others: some apps publish a trigger for points being earned, many publish none at all and only offer actions. If yours offers nothing to start from, there is no recipe here that will help, and that is a limit of the app rather than of Shopify Flow.
Points earned:
{"events":[{"name":"Loyalty Points Earned","email":"{{ customer.defaultEmailAddress.emailAddress }}","properties":{"points":{{ trigger.points }},"balance":{{ trigger.balance }}}}]}
Points redeemed:
{"events":[{"name":"Loyalty Points Redeemed","email":"{{ customer.defaultEmailAddress.emailAddress }}","properties":{"points":{{ trigger.points }},"balance":{{ trigger.balance }}}}]}
Building one, step by step
- In your Shopify admin, open Apps, then Shopify Flow, then Create workflow.
- Choose the trigger your store-credit, review or loyalty app publishes.
- Add the Send HTTP request action.
- Set the method to
POST— the action starts asGET, and aGETwith a body fails with "Invalid argument for HTTP request" — and paste the URL from the table above. - Add the two headers. Use Add secret for the key.
- Paste the recipe's body into the Body box, as one line.
- For each variable in it, select the placeholder — the braces and everything between them — and replace it using Add variable. Typing a variable by hand does not work; Shopify Flow refuses it. For the person's email, the validated path is Customer → defaultEmailAddress → emailAddress — search the picker for
emailAddressand choose that one; it is the same path in every recipe on this page. The plainemailfield sits under "Deprecated fields" and is not the one to use. Choose single values only — a list, such as a customer's tags, is inserted as a loop that breaks the body. - If a value the recipe names is not offered, delete that property rather than leaving a placeholder behind. A leftover placeholder makes the whole request fail.
- Turn the workflow on.
There is no file to import. Shopify Flow can import a workflow another store exported, but the file carries a signature only Shopify can write, so nobody outside Shopify can hand you one that will open. Build it once from the steps above — and once you have, you can export your own copy from More actions → Export and keep it as a backup or use it on a second store.
Checking it worked
Make the thing happen — issue a small credit, leave a test review — and then look in two places.
Shopify Flow's own run history tells you whether the request went out and what came back. A 202 is us accepting it. A 401 means the key is wrong or missing. A 422 means the body did not parse — almost always a placeholder left in, or a value that came through empty.
The Metrics page lists the event by name. Metrics appear the first time one arrives, so until then there is nothing to find — and a metric that has arrived but has no history yet reads No activity yet.
Once the first one lands, the name is also offered in the segment builder and in the trigger list when you build an automation.
What to expect, honestly
An event for an address you have never seen creates a profile, and that profile is not confirmed. We take your word that the event happened; we have no way to take your word that the address belongs to a real person who wants mail. So the new profile is held out of your counts and out of every send until something confirms it — an order, or one of your own forms. The Verified identity page explains what confirms a profile and why.
For an address you already have, nothing new is created. The event attaches to the profile that is already there, and everything about that profile is unchanged.
Nothing sent this way counts as consent. An event says something happened. Whether you may email someone is a separate record, and this door never touches it.
A few names are refused. The events this app records for itself — orders, checkouts, opens, clicks and the like — cannot be written from outside, because a rule about who bought something has to be able to trust what it counts. If you send one of those names, we accept the request and record nothing. The names on this page are not among them.
We do not check the same event twice. If Shopify Flow retries a request, you get the event twice unless you also send an external_id — a value unique to that credit or that review — in which case the second one is ignored.