Shopify App Billing
Shopify's app platform supports charging Shopify merchants for using your application. Applications built using Gadget's Shopify Connection use the existing Shopify Billing APIs for creating charges, and money is moved using Shopify's standard partner payout system.
Read about Shopify's billing API here in Shopify's docs.
Different applications may choose from a variety of different billing schemes, all of which are supported by Gadget. You must choose which billing scheme makes the most sense for your application, and then create the right AppSubscription
or AppPurchaseOneTime
objects using the Shopify GraphQL API to implement your billing scheme. You also may need to build a frontend interface for users to select from among your plans, upgrade and downgrade, and view their usage of your application.
Scheme | Suggested Implementation | |
---|---|---|
Free app | No action required | |
Fixed Monthly fee | Create AppSubscription in the Create action of the Shopify Shop model Success Effects Effect | Shopify docs |
One time fee | Create AppPurchaseOneTime in the Create action of the Shopify Shop model Success Effects Effect | Shopify docs |
Usage charge per key action | Create AppPurchaseOneTime in key actions within Shopify or your own models | Shopify docs |
2 or more plans with monthly fees | Create AppSubscription in a new Subscribe Action on the Shopify Shop model | Shopify docs |
Free trial, monthly fee after | Check Shop install date within actions, show a plan selection screen in your application, add a Subscribe action to the Shopify Shop model and create an AppSubscription for your plan in it | Shopify docs |
Want to see a working example using test charges? You can fork this Gadget project and try it out yourself. Replace the
billing-tutorial.gadget.app
domain in oninstall.js
and onsubscribe.js
with your app domain.
A Shopify CLI 3.0 app is also available to clone and embed in a store's admin. See the CLI project's README for details on cloning and setting up the app.
Charge flow
Charges created via API calls to Shopify must be accepted by merchants before they become active and money starts moving. Freshly created charge objects return a
confirmationUrl
URL that you must send merchants to where they can accept the charge. Before a charge object has been accepted, they
exist in a pending
state, and a merchant won't always accept them. You should only consider payment confirmed after the merchant has
accepted the charge. Shopify will send merchants back to the returnUrl
property sent in during charge creation.
Read more about the billing process in Shopify's docs.
Creating charges
Charge objects which result in merchants paying you money are created by making calls to the Shopify API. You can use Gadget's existing API client object to do this within action effects and HTTP routes with the connections.shopify.current
object.
For example, we can create an AppSubscription
as soon as our application gets installed
by adding a new Run Code effect to the Success Effects of the Create action on the Shop model. This effect will make a call to Shopify, and get back a confirmationUrl
to send the merchant to. We send or store this URL, send the merchant to it on the frontend, and if the merchant accepts the charge, Shopify will send them to the /finished-payment
HTTP route, where we can set up their access to the application.
models/shopifyShop/installed/createAppCharge.jsJavaScript1module.exports = async ({ api, record, connections, logger }) => {2 // get an instance of the shopify-api-node API client for this shop3 const shopify = connections.shopify.current;45 // make an API call to Shopify to create a charge object6 const result = await shopify.graphql(`7 mutation {8 appPurchaseOneTimeCreate(9 name: "Basic charge"10 price: { amount: 100.00, currencyCode: USD }11 returnUrl: "https://my-gadget-slug.gadget.app/finished-payment?shop_id=${connections.shopify.currentShopId}"12 ) {13 userErrors {14 field15 message16 }17 confirmationUrl18 appPurchaseOneTime {19 id20 }21 }22 }23`);2425 const { confirmationUrl, appPurchaseOneTime } = result.appPurchaseOneTimeCreate;2627 // store the `result.confirmationUrl` that the merchant needs to visit28 await api.internal.shopifyShop.update(record.id, {29 shopifyShop: { confirmationUrl },30 });3132 logger.info(33 { appPurchaseOneTimeId: appPurchaseOneTime.id },34 "created one time app purchase"35 );36};
See Accessing the Shopify API for more info on using the connections.shopify
object.
To grant a merchant who has accepted this charge to our application, we can set up some state in our database in a GET-finished-payment.js
HTTP route in our Gadget app. This route powers the returnUrl
we send into Shopify when we create the charge object above.
routes/GET-finished-payment.jsJavaScript1module.exports = async (request, reply) => {2 const { api, connections } = request;3 // get an instance of the shopify-api-node API client for this shop4 const shopify = await connections.shopify.forShopId(request.params.shop_id);56 // make an API call to Shopify to validate that the charge object for this shop is active7 const result = await shopify.graphql(`8 query {9 node(id: "gid://shopify/AppSubscription/${request.query.charge_id}") {10 id11 ... on AppSubscription {12 status13 }14 }15 }16 `);1718 if (result.node.status != "ACTIVE") {19 // the merchant has not accepted the charge, so we can show them a message20 await reply.code(400).send("Invalid charge ID specified");21 return;22 }23 // the merchant has accepted the charge, so we can grant them access to our application24 // example: mark the shop as paid by setting a `plan` attribute, this may vary for your billing model25 await api.internal.shopifyShop.update(request.params.shop_id, {26 shopifyShop: { plan: "basic" },27 });2829 // send the user back to the embedded app30 await reply.redirect("/");31};
Free apps
Apps that don't need to charge merchants don't need to create any app charges. Merchants will install your application and complete the OAuth process, and then your application can begin working on behalf of the merchant with no other intervention needed. If you begin to start charging in the future, you can later create charges for existing merchants using these recipes.
Subscribing to a recurring plan
Shopify's partner billing API supports charging merchants a fee every month for access to your application. For example, a product quiz application could charge $15.00 per month for the ability to run one quiz. If your application has multiple different plans merchants can select or have a free option and a paid option, you need to implement functionality for merchants to select plans, and then create recurring charges depending on which plan is selected. This plan selection interface is implemented within your application once it is installed. A merchant will install your application from the Shopify App Store, complete the OAuth process, and Gadget will create a Shopify Shop record in your application's database. Once the app is installed, merchants can visit your application in their Shopify Admin, and you can then show them your plan selection interface.
Within your plan selection interface, you can then make a call back to your application with the plan a merchant has selected. Gadget recommends adding a Subscribe action to your Shopify Shop model which you can then call from your frontend, and creating an AppSubscription
resource in the Shopify API to represent the selected plan.
For example, in our Gadget backend, we can create several things:
- a new string Field on the Shopify Shop model called Plan to track which plan a merchant has selected
- a new string Field on the Shopify Shop model called Confirmation URL to track where to send the merchant to confirm the payment information for their plan
- a Subscribe action on the Shopify Shop model for implementing plan selection.
- a
routes/GET-finalize-payment.js
HTTP route to use as thereturnUrl
when creating charges for confirming charge acceptance
First, let's make a Subscribe action on the Shopify Shop model. We can add an effect that creates the charge with Shopify for presenting to the merchant:
models/shopifyShop/subscribe/createAppCharge.jsJavaScript1const PLANS = {2 basic: {3 price: 10.0,4 },5 pro: {6 price: 20.0,7 },8 enterprise: {9 price: 100.0,10 },11};1213/**14 * Effect code for subscribe on Shopify Shop15 * @param { import("gadget-server").SubscribeShopifyShopActionContext } context - Everything for running this effect, like the api client, current record, params, etc. More on effect context: https://docs.gadget.dev/guides/extending-with-code#effect-context16 */17module.exports = async ({ api, record, params, connections, logger }) => {18 // get the plan object from the list of available plans19 const name = params.plan;20 const plan = PLANS[name];21 if (!plan) throw new Error(`unknown plan name ${name}`);2223 // get an instance of the shopify-api-node API client for this shop24 const shopify = connections.shopify.current;2526 const CREATE_SUBSCRIPTION_QUERY = `27 mutation CreateSubscription($name: String!, $price: Decimal!) {28 appSubscriptionCreate(29 name: $name,30 test: true,31 returnUrl: "http://my-app-slug.gadget.app/finished-subscription?shop_id=${connections.shopify.currentShopId}",32 lineItems: [{33 plan: {34 appRecurringPricingDetails: {35 price: { amount: $price, currencyCode: USD }36 interval: EVERY_30_DAYS37 }38 }39 }]40 ) {41 userErrors {42 field43 message44 }45 confirmationUrl46 appSubscription {47 id48 }49 }50 }51`;5253 // make an API call to Shopify to create a charge object54 const result = await shopify.graphql(CREATE_SUBSCRIPTION_QUERY, {55 name,56 price: plan.price,57 });5859 const { confirmationUrl, appSubscription } = result.appSubscriptionCreate;6061 // update this shop record to send the confirmation URL back to the frontend62 await api.internal.shopifyShop.update(record.id, {63 shopifyShop: { confirmationUrl },64 });6566 logger.info({ appSubscriptionId: appSubscription.id }, "created subscription");67};6869// add a paramter to this action to accept which plan name the merchant has selected70module.exports.params = {71 plan: { type: "string" },72};
With this effect in place, we need to implement the returnUrl
Shopify will send merchants who have accepted the charge to. This URL is where we should mark the shop as being on a plan, as this is the point at which we know the merchant will be charged. We add a routes/GET-finished-payment.js
file to match the /finished-payment
portion of the returnUrl
specified when we created the charge:
routes/GET-finished-payment.jsJavaScript1module.exports = async (request, reply) => {2 const { api, connections } = request;3 const { shop_id, charge_id } = request.query;45 // get an instance of the shopify-api-node API client for this shop6 const shopify = await connections.shopify.forShopId(request.params.shop_id);78 // make an API call to Shopify to validate that the charge object for this shop is active9 const result = await shopify.graphql(`10 query {11 node(id: "gid://shopify/AppSubscription/${request.query.charge_id}") {12 id13 ... on AppSubscription {14 status15 name16 }17 }18 }19 `);2021 if (result.node.status != "ACTIVE") {22 // the merchant has not accepted the charge, so we can show them a message23 await reply.code(400).send("Invalid charge ID specified");24 return;25 }26 // the merchant has accepted the charge, so we can grant them access to our application27 // mark the shop as paid by setting the `plan` attribute to the charged plan namemodel28 await api.internal.shopifyShop.update(request.params.shop_id, {29 shopifyShop: { plan: result.node.name },30 });3132 // send the user back to the embedded app, this URL may be different depending on where your frontend is hosted33 await reply.redirect("/");34};
With the Subscribe action and confirmation pieces in place, we now need to trigger the new Subscribe action from our merchant-facing frontend somewhere. We need to run the subscribe action to create the recurring charge and then redirect the merchant to Shopify's confirmation page to accept the charge. If you're using your app's JavaScript client, you could run:
JavaScript// use the useNavigate hook from the @shopify/app-bridge-react npm package to handle the redirectconst navigate = useNavigate();const shop = await api.shopifyShop.subscribe(theShopId, { plan: "basic" });navigate(shop.confirmationUrl);
Or if you're using React, you could run this action with the @gadgetinc/react React hooks package in a React component:
JavaScript1import { useNavigate } from "@shopify/app-bridge-react";23export const PlanSelectorButton = (props) => {4 const [{ fetching, error, data }, createSubscription] = useAction(5 api.shopifyShop.subscribe6 );7 const navigate = useNavigate();89 const subscribe = useCallback(async (plan) => {10 // create the resource in the backend11 const shop = await createSubscription(theShopId, { plan });12 // redirect the merchant to accept the charge within Shopify's interface13 navigate(shop.confirmationUrl);14 });1516 return (17 <button18 onClick={() => {19 subscribe("basic");20 }}21 disabled={fetching}22 >23 Basic24 </button>25 );26};
Upgrades and downgrades
Merchants may at some point decide they need more or less of your app's features over time. If you offer multiple different plans, you need a plan selection interface that merchants who have already selected a plan can revisit to select a new plan. Plan upgrading or downgrading is implemented natively by Shopify, and the new plan is registered in the same way as an existing plan. Shopify allows your app to have only one AppSubscription
object created at a time. This means that when a merchant changes plans and you send an API call to Shopify to create a new AppSubscription
, it will automatically replace the old one, and the merchant will only be charged for the new amount on the new AppSubscription
object.
For example, say a merchant is upgrading from a Basic plan costing $5 a month to a Pro plan costing $10 a month. When the merchant first installed your app, your app will have created the $5 AppSubscription
. When they revisit the plan selector and select the $10 plan, your app can immediately create a new $10 AppSubscription
, and Shopify will replace the $5 subscription and figure out the prorating and billing cycle details. You don't need to delete the $5 AppSubscription
object yourself.
Read more about plan changes and prorating in Shopify's docs.
Preventing access without payment
Shopify's API requires you to allow merchants to install your application before they have set up payment terms with you. This means that your app will technically be installed on merchants who may have not selected a plan, or who may have enjoyed their free trial but not yet selected a plan. This means you should disable access to the key parts of your application until a merchant has selected a plan, and encourage them to do so.
Access to reading records from your application disabled using Model Filters, and app behaviour can be disabled using Run Code effects in your actions.
For example, we may choose to store a merchant's payment state in a Plan string field on the Shopify Shop model. When a merchant first installs the application, the plan
will be null
, and then when they select a plan and accept the charge, the app can update plan
field to hold whichever plan they selected. With this in place, you can begin to conditionally perform your application's duties for paying customers. For example, for an application which analyzes order fraud, we can only do the fraud analysis if the merchant has selected a current plan:
1module.exports = async ({ api, record, connections }) => {2 // `record` is a Shopify Order record, load the Shopify Shop record for this order3 const shop = await api.shopifyShop.findOne(connections.shopify.currentShopId);45 // only do the processing for this action if the shop is on a paid plan6 if (shop.plan !== null) {7 await doFraudAnalysis(record);8 }9 // otherwise, the shop hasn't selected a plan and isn't paying, don't perform the analysis10};
If need be, you can also extend your model read permissions to prevent access to records unless the merchant is on a paid plan. You can update the Gelly model filter snippet in the Roles & Permissions section of your Gadget app to only return records for paid merchants. For example, if you have a Fraud Result model which belongs to the Shopify Shop model, you can only return Fraud Result records for merchants who are on a paid plan:
gellyfragment Filter($session: Session) on FraudResult {*[where !isNull(shop.plan)]}
Often, you may want to disable access to your application's merchant-facing frontend if the merchant hasn't yet paid for the application. Using React, this can be done using a wrapper component around your app which checks plan status for every page the merchant tries to access:
components/SubscriptionWrapper.jsxJavaScript1import { Layout, Page, Spinner, Banner } from "@shopify/polaris";2import { PlanSelector } from "./PlanSelector"; // up to you to implement34export const SubscriptionWrapper = (props) => {5 const [{ fetching, data: currentShop }] = useFindOne(6 api.shopifyShop,7 props.shopId8 );9 // if we're loading the current shop data, show a spinner10 if (fetching) {11 return (12 <Page>13 <Spinner />14 </Page>15 );16 }1718 // if the shop has selected a plan, render the app and don't bug the merchant about plans19 if (currentShop.plan) {20 return props.children;21 } else {22 // the merchant has not paid for the application and should be denied access, show them the plan selection interface instead of the app23 return (24 <Page>25 <Layout>26 <Banner status="warning">27 You must select a plan to continue using this application28 </Banner>29 <PlanSelector />30 </Layout>31 </Page>32 );33 }34};
By default, Gadget will continue to receive webhooks and run syncs for any shop with the app installed. This will keep data for the shop up to date, and keep any free functionality of your application working as usual. But, it can cost you money or allow merchants to use your app without paying, so it may be necessary to disable this functionality for merchants who don't have access. If you want to disable webhook processing or syncing, you'll need to add effects to the Shopify model actions or the Shopify Sync model actions to prevent processing.
One-time charges
Shopify allows applications one-time fees that don't automatically subscribe the merchant to anything. For example, you could charge a merchant $10 upfront to use your app forever, $10 to process 1000 orders, or $100 for an extra theme customization. One-time fees like this are created one at a time by making calls to the Shopify API, allowing merchants to pay-as-you-go, which they sometimes prefer.
Calls should generally be made to Shopify's GraphQL API to create usage-based charges infrequently because the merchant must confirm each usage-based charge. It'd be a bad user experience for the merchant to have to confirm a $0.10 charge for each order they process, so instead Gadget recommends recurring billing, or selling chunks of usage, like $10 for processing 1000 orders. Your application then must track how often it performs the processing, and calculate how much usage is remaining.
One-time charges are implemented using the AppPurchaseOneTime
object in the Shopify API. One-time charges can be created using the connections.shopify
object present within Effects and HTTP routes with the appPurchaseOneTimeCreate
Shopify GraphQL mutation.
For example, if we're building an application which charges one small fee upfront, we need to do two things to charge the merchant:
- add a string field to the Shopify Shop object to store the confirmation URL to pass to the merchant
- and add a Success Effect on the Create action within the Shopify Shop model to create the charge:
models/shopifyShop/installed/createAppCharge.jsJavaScript1module.exports = async ({ api, record, connections }) => {2 // get an instance of the shopify-api-node API client for this shop3 const shopify = connections.shopify.current;45 // make an API call to Shopify to create a charge object6 const result = await shopify.graphql(`7 mutation {8 appPurchaseOneTimeCreate(9 name: "Basic charge"10 price: { amount: 100.00, currencyCode: USD }11 returnUrl: "https://my-app-slug.gadget.app/finished-payment?shop_id=${connections.shopify.currentShopId}"12 ) {13 userErrors {14 field15 message16 }17 confirmationUrl18 appPurchaseOneTime {19 id20 }21 }22 }23 `);2425 const { confirmationUrl, appPurchaseOneTime } = result.appPurchaseOneTimeCreate;2627 // store the `result.confirmationUrl` that the merchant needs to visit28 await api.internal.shopifyShop.update(record.id, {29 shopifyShop: { confirmationUrl },30 });3132 logger.info(33 { appPurchaseOneTimeId: appPurchaseOneTime.id },34 "created one time app purchase"35 );36};
Then on the frontend, we can access the shop's confirmationUrl
property and redirect the merchant to this URL to have them confirm the charge.
JavaScriptconst shop = await api.shopifyShop.findOne(someShopId);// use the useNavigate hook from the @shopify/app-bridge-react npm package to handle the redirectconst navigate = useNavigate();navigate(shop.confirmationUrl);
Or if you're using React, you could run this action with the @gadgetinc/react React hooks package in a React component:
JavaScript1import { useNavigate } from "@shopify/app-bridge-react";23export const RedirectToConfirmationURL = (props) => {4 const [{ fetching, error, data }] = useFindOne(api.shopifyShop, theShopId);5 const navigate = useNavigate();67 if (data.confirmationUrl) {8 navigate(data.confirmationUrl);9 }10};
Implementing a free trial
Free trials are a great growth tool for Shopify applications so merchants can see the value an application adds before having to make the decision to pay for it. Shopify has limited native support for free trials, that allow you to start a merchant on a particular plan where the merchant will only begin being charged after some days have passed.
Free trials using Shopify's native support are registered using the same API calls as a normal recurring monthly charge. An easy way to set this up in Gadget would be to add a Success Effect to the Create action on the Shopify Shop model that creates a recurring monthly charge with the trialDays
property set:
models/shopifyShop/install/startTrial.jsJavaScript1module.exports = async ({ api, record, connections, logger }) => {2 const result = await connections.shopify.current.graphql(`3 mutation {4 appSubscriptionCreate(5 name: "Recurring Plan with 7 day trial",6 trialDays: 7,7 returnUrl: "http://my-gadget-slug.gadget.app",8 lineItems: [{9 plan: {10 appRecurringPricingDetails: {11 price: { amount: 10.00, currencyCode: USD }12 }13 }14 }]15 ) {16 userErrors {17 field18 message19 }20 confirmationUrl21 appSubscription {22 id23 }24 }25 };26 `);2728 const { appSubscription } = result.appSubscriptionCreate;29 logger.info(30 { appSubscriptionId: result.appSubscription.id },31 "created app subscription"32 );33};
See Shopify's docs on free trials
Advanced free trials
While Shopify has native free trial support built in, it doesn't support the following commonly required features:
- reminders to the merchant to pay for the app during the trial
- tracking for which merchants have already used a free trial
Gadget allows you to customize your application's free trial experience to try to drive more merchant conversions.
If you want to show developers how long is left in their free trial, you need to track when a free trial started. Gadget recommends adding a new date / time Trial Started At field to your Shopify Shop model to track when each merchant started their trial. In the Create action for the Shopify Shop model, you can populate this field so you can later check against it:
1module.exports = async ({ api, record }) => {2 if (!record.trialStartedAt) {3 // record the current time as the trial start date4 await api.internal.shopifyShop.update(record.id, { trialStartedAt: new Date() });5 }6};
The above code example will not restart a shop's trial if they install your application a second time, which prevents nefarious merchants from uninstalling and reinstalling your application repeatedly to avoid having to pay.
Second, during a free trial, it is important to reveal to the merchant that they are in fact on a free trial, and they'll start paying at the end of the free trial. This is most often done within the merchant-facing frontend of your application with a banner or similar which gives the merchant more information or guides them into a plan selection interface. Once the merchant has selected a plan, the banner should be hidden. This can be done with a React component which always fetches the current shop and inspects the plan state:
JavaScript1import { Banner } from "@shopify/polaris";23const trialLength = 7; // days45export const PlanSelectionBanner = (props) => {6 const [{ fetching, data: currentShop }] = useFindOne(7 api.shopifyShop,8 props.shopId9 );10 if (fetching) return null;11 if (!currentShop.plan) {12 const daysUntilTrialOver = Math.floor(13 (new Date().getTime() - shop.trialStartedAt.getTime()) / (1000 * 3600 * 24)14 );1516 return (17 <Banner>18 <p>19 You have {daysUntilTrialOver} many day(s) left on your free trial. Please{" "}20 <a href="/select-a-plan">select a plan</a> to keep using this great app!21 </p>22 </Banner>23 );24 }25 return null;26};
With this tracking of a merchant's trial start date in place, you can implement a plan selection screen and an Action to power the actual plan selection. See Subscribing to a recurring plan for details on implementing plan selection.
And finally, with a trial duration tracking and plan selection implemented, Gadget suggests denying access to your application to merchants whose trials have expired without selecting a plan. You can disable backend logic using the details in Preventing access without payment, and you can implement frontend logic to force plan selection in your merchant-facing frontend.
Using React, this can be done using a wrapper component around your app which checks the plan status for every page the merchant tries to access:
components/SubscriptionWrapper.jsxJavaScript1import { CalloutCard, Layout, Page, Spinner } from "@shopify/polaris";2import { PlanSelector } from "./PlanSelector"; // up to you to implement34// this would replace the PlanSelectionBanner component above5export const SubscriptionWrapper = (props) => {6 const [{ fetching, data: currentShop }] = useFindOne(7 api.shopifyShop,8 props.shopId9 );10 // if we're loading the current shop data, show a spinner11 if (fetching) {12 return (13 <Page>14 <Spinner />15 </Page>16 );17 }1819 // if the shop has selected a plan, render the app and don't bug the merchant about plans20 if (currentShop.plan) return props.children;2122 const daysUntilTrialOver = Math.floor(23 (new Date().getTime() - shop.trialStartedAt.getTime()) / (1000 * 3600 * 24)24 );25 if (daysUntilTrialOver > 0) {26 // the merchant is on a free trial, show the app and a banner encouraging them to select a plan27 return (28 <>29 {props.children}30 <Banner>31 You have {daysUntilTrialOver} many day(s) left on your free trial. Please{" "}32 <a href="#">select a plan</a> to keep using this great app!33 </Banner>34 </>35 );36 } else {37 // the merchant's trial has expired, show them the plan selection interface, don't show them the app38 return (39 <Page>40 <Notification>41 Your trial has expired, please select a plan to continue using the42 application43 </Notification>44 <PlanSelector />45 </Page>46 );47 }48};
Crediting merchants
Occasionally, application developers will want to give a credit to individual merchants. They may offer a refund when a merchant contacts them to cancel, or discount the product for a potentially high-value customer. Credits are implemented with the AppCredit
object created using the Shopify API.
Generally, credits are given to merchants manually by administrators, so there's no merchant-facing UI to build. Sometimes it's easiest to create credits manually using handcrafted API requests to Shopify's API, but if you'd like to build an easier-to-use interface for crediting, Gadget recommends adding a Credit action on the Shopify Shop model. You can then add a code effect to create an AppCredit
object for some amount.
For example, we could add this code effect to a new Credit action on the Shopify Shop model:
models/shopifyShop/credit/createAppCredit.jsJavaScript1module.exports = async ({ api, record, params, connections, logger }) => {2 // get an instance of the shopify-api-node API client for this shop3 const shopify = connections.shopify.current;45 // make an API call to Shopify to create a charge object6 const result = await shopify.graphql(7 `8 mutation CreateCredit($amount: Decimal!) {9 appCreditCreate(10 description: "application credit"11 amount: {12 amount: $amount,13 currencyCode: USD14 }15 test: true16 ) {17 userErrors {18 field19 message20 }21 appCredit {22 id23 }24 }25 }26 `,27 { amount: params.amount }28 );2930 const { appCredit } = result.appCreditCreate;3132 logger.info({ amount: params.amount, creditID: appCredit.id }, "credited shop");33};3435// make this effect take an amount parameter for the amount to credit36module.export.params = {37 amount: { type: "number" },38};
We could then invoke this action using the Gadget GraphQL API in an internal, staff-only frontend:
await api.shopifyShop.credit(someShopId, { amount: 10 });
Read more about creating credits in Shopify's docs.