Adminobject
Contains methods for authenticating and interacting with the Admin API.
This function can handle requests for apps embedded in the Admin, Admin extensions, or non-embedded apps.
Authenticates requests coming from the Shopify admin.
The shape of the returned object changes depending on the config.
Anchor to authenticate.admin-parametersParameters
- Anchor to requestrequestRequestrequired
AdminContext
EmbeddedTypedAdminContext<Config> & ScopesContext
EmbeddedTypedAdminContext
Config['isEmbeddedApp'] extends false
? NonEmbeddedAdminContext<Config>
: EmbeddedAdminContext<Config>
NonEmbeddedAdminContext
- admin
Methods for interacting with the GraphQL Admin API for the store that made the request.
AdminApiContext
- billing
Billing methods for this store, based on the plans defined in the `billing` config option.
BillingContext<Config>
- cors
A function that ensures the CORS headers are set correctly for the response.
EnsureCORSFunction
- session
The session for the user who made the request. This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice. Use this to get shop or user-specific data.
Session
export interface NonEmbeddedAdminContext<Config extends AppConfigArg>
extends AdminContextInternal<Config> {}
AdminApiContext
- graphql
Methods for interacting with the Shopify Admin GraphQL API
GraphQLClient<AdminOperations>
export interface AdminApiContext {
/**
* Methods for interacting with the Shopify Admin GraphQL API
*
* {@link https://shopify.dev/docs/api/admin-graphql}
* {@link https://github.com/Shopify/shopify-app-js/blob/main/packages/apps/shopify-api/docs/reference/clients/Graphql.md}
*
* @example
* <caption>Querying the GraphQL API.</caption>
* <description>Use `admin.graphql` to make query / mutation requests.</description>
* ```ts
* // /app/routes/**\/*.ts
* import { ActionFunctionArgs } from "@remix-run/node";
* import { authenticate } from "../shopify.server";
*
* export const action = async ({ request }: ActionFunctionArgs) => {
* const { admin } = await authenticate.admin(request);
*
* const response = await admin.graphql(
* `#graphql
* mutation populateProduct($input: ProductInput!) {
* productCreate(input: $input) {
* product {
* id
* }
* }
* }`,
* {
* variables: {
* input: { title: "Product Name" },
* },
* },
* );
*
* const productData = await response.json();
* return json({
* productId: productData.data?.productCreate?.product?.id,
* });
* }
* ```
*
* ```ts
* // /app/shopify.server.ts
* import { shopifyApp } from "@shopify/shopify-app-remix/server";
*
* const shopify = shopifyApp({
* // ...
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*
* @example
* <caption>Handling GraphQL errors.</caption>
* <description>Catch `GraphqlQueryError` errors to see error messages from the API.</description>
* ```ts
* // /app/routes/**\/*.ts
* import { ActionFunctionArgs } from "@remix-run/node";
* import { authenticate } from "../shopify.server";
*
* export const action = async ({ request }: ActionFunctionArgs) => {
* const { admin } = await authenticate.admin(request);
*
* try {
* const response = await admin.graphql(
* `#graphql
* query incorrectQuery {
* products(first: 10) {
* nodes {
* not_a_field
* }
* }
* }`,
* );
*
* return json({ data: await response.json() });
* } catch (error) {
* if (error instanceof GraphqlQueryError) {
* // error.body.errors:
* // { graphQLErrors: [
* // { message: "Field 'not_a_field' doesn't exist on type 'Product'" }
* // ] }
* return json({ errors: error.body?.errors }, { status: 500 });
* }
* return json({ message: "An error occurred" }, { status: 500 });
* }
* }
* ```
*
* ```ts
* // /app/shopify.server.ts
* import { shopifyApp } from "@shopify/shopify-app-remix/server";
*
* const shopify = shopifyApp({
* // ...
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*/
graphql: GraphQLClient<AdminOperations>;
}
GraphQLClient
- query
Operation extends keyof Operations
- options
GraphQLQueryOptions<Operation, Operations>
interface Promise<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
/**
* Attaches a callback for only the rejection of the Promise.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of the callback.
*/
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}, interface Promise<T> {}, Promise: PromiseConstructor, interface Promise<T> {
readonly [Symbol.toStringTag]: string;
}, interface Promise<T> {
/**
* Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The
* resolved value cannot be modified from the callback.
* @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected).
* @returns A Promise for the completion of the callback.
*/
finally(onfinally?: (() => void) | undefined | null): Promise<T>;
}
<
Operation extends keyof Operations,
>(
query: Operation,
options?: GraphQLQueryOptions<Operation, Operations>,
) => Promise<GraphQLResponse<Operation, Operations>>
GraphQLQueryOptions
- apiVersion
The version of the API to use for the request.
ApiVersion
- headers
Additional headers to include in the request.
Record<string, any>
- signal
An optional AbortSignal to cancel the request.
AbortSignal
- tries
The total number of times to try the request if it fails.
number
- variables
The variables to pass to the operation.
ApiClientRequestOptions<Operation, Operations>["variables"]
export interface GraphQLQueryOptions<
Operation extends keyof Operations,
Operations extends AllOperations,
> {
/**
* The variables to pass to the operation.
*/
variables?: ApiClientRequestOptions<Operation, Operations>['variables'];
/**
* The version of the API to use for the request.
*/
apiVersion?: ApiVersion;
/**
* Additional headers to include in the request.
*/
headers?: Record<string, any>;
/**
* The total number of times to try the request if it fails.
*/
tries?: number;
/**
* An optional AbortSignal to cancel the request.
*/
signal?: AbortSignal;
}
BillingContext
- cancel
Cancels an ongoing subscription, given its ID.
(options: CancelBillingOptions) => Promise<AppSubscription>
- check
Checks if the shop has an active payment for any plan defined in the `billing` config option.
<Options extends CheckBillingOptions<Config>>(options?: Options) => Promise<BillingCheckResponseObject>
- createUsageRecord
Creates a usage record for an app subscription.
(options: CreateUsageRecordOptions) => Promise<UsageRecord>
- request
Requests payment for the plan.
(options: RequestBillingOptions<Config>) => Promise<never>
- require
Checks if the shop has an active payment for any plan defined in the `billing` config option.
(options: RequireBillingOptions<Config>) => Promise<BillingCheckResponseObject>
- updateUsageCappedAmount
Updates the capped amount for a usage billing plan.
(options: UpdateUsageCappedAmountOptions) => Promise<never>
export interface BillingContext<Config extends AppConfigArg> {
/**
* Checks if the shop has an active payment for any plan defined in the `billing` config option.
*
* @returns A promise that resolves to an object containing the active purchases for the shop.
*
* @example
* <caption>Requesting billing right away.</caption>
* <description>Call `billing.request` in the `onFailure` callback to immediately redirect to the Shopify page to request payment.</description>
* ```ts
* // /app/routes/**\/*.ts
* import { LoaderFunctionArgs } from "@remix-run/node";
* import { authenticate, MONTHLY_PLAN } from "../shopify.server";
*
* export const loader = async ({ request }: LoaderFunctionArgs) => {
* const { billing } = await authenticate.admin(request);
* await billing.require({
* plans: [MONTHLY_PLAN],
* isTest: true,
* onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),
* });
*
* // App logic
* };
* ```
* ```ts
* // shopify.server.ts
* import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
*
* export const MONTHLY_PLAN = 'Monthly subscription';
*
* const shopify = shopifyApp({
* // ...etc
* billing: {
* [MONTHLY_PLAN]: {
* lineItems: [
* {
* amount: 5,
* currencyCode: 'USD',
* interval: BillingInterval.Every30Days,
* }
* ],
* },
* }
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*
* @example
* <caption>Redirect to a plan selection page.</caption>
* <description>When the app has multiple plans, create a page in your App that allows the merchant to select a plan. If a merchant does not have the required plan you can redirect them to page in your app to select one.</description>
* ```ts
* // /app/routes/**\/*.ts
* import { LoaderFunctionArgs, redirect } from "@remix-run/node";
* import { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from "../shopify.server";
*
* export const loader = async ({ request }: LoaderFunctionArgs) => {
* const { billing } = await authenticate.admin(request);
* const billingCheck = await billing.require({
* plans: [MONTHLY_PLAN, ANNUAL_PLAN],
* isTest: true,
* onFailure: () => redirect('/select-plan'),
* });
*
* const subscription = billingCheck.appSubscriptions[0];
* console.log(`Shop is on ${subscription.name} (id ${subscription.id})`);
*
* // App logic
* };
* ```
* ```ts
* // shopify.server.ts
* import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
*
* export const MONTHLY_PLAN = 'Monthly subscription';
* export const ANNUAL_PLAN = 'Annual subscription';
*
* const shopify = shopifyApp({
* // ...etc
* billing: {
* [MONTHLY_PLAN]: {
* lineItems: [
* {
* amount: 5,
* currencyCode: 'USD',
* interval: BillingInterval.Every30Days,
* }
* ],
* },
* [ANNUAL_PLAN]: {
* lineItems: [
* {
* amount: 50,
* currencyCode: 'USD',
* interval: BillingInterval.Annual,
* }
* ],
* },
* }
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*/
require: (
options: RequireBillingOptions<Config>,
) => Promise<BillingCheckResponseObject>;
/**
* Checks if the shop has an active payment for any plan defined in the `billing` config option.
*
* @returns A promise that resolves to an object containing the active purchases for the shop.
*
* @example
* <caption>Check what billing plans a merchant is subscribed to.</caption>
* <description>Use billing.check if you want to determine which plans are in use. Unlike `require`, `check` does not
* throw an error if no active billing plans are present. </description>
* ```ts
* // /app/routes/**\/*.ts
* import { LoaderFunctionArgs } from "@remix-run/node";
* import { authenticate, MONTHLY_PLAN } from "../shopify.server";
*
* export const loader = async ({ request }: LoaderFunctionArgs) => {
* const { billing } = await authenticate.admin(request);
* const { hasActivePayment, appSubscriptions } = await billing.check({
* plans: [MONTHLY_PLAN],
* isTest: false,
* });
* console.log(hasActivePayment);
* console.log(appSubscriptions);
* };
* ```
* ```ts
* // shopify.server.ts
* import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
*
* export const MONTHLY_PLAN = 'Monthly subscription';
* export const ANNUAL_PLAN = 'Annual subscription';
*
* const shopify = shopifyApp({
* // ...etc
* billing: {
* [MONTHLY_PLAN]: {
* lineItems: [
* {
* amount: 5,
* currencyCode: 'USD',
* interval: BillingInterval.Every30Days,
* }
* ],
* },
* [ANNUAL_PLAN]: {
* lineItems: [
* {
* amount: 50,
* currencyCode: 'USD',
* interval: BillingInterval.Annual,
* }
* ],
* },
* }
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*
* @example
* <caption>Check for payments without filtering.</caption>
* <description>Use billing.check to see if any payments exist for the store, regardless of whether it's a test or
* matches one or more plans.</description>
* ```ts
* // /app/routes/**\/*.ts
* import { LoaderFunctionArgs } from "@remix-run/node";
* import { authenticate, MONTHLY_PLAN } from "../shopify.server";
*
* export const loader = async ({ request }: LoaderFunctionArgs) => {
* const { billing } = await authenticate.admin(request);
* const { hasActivePayment, appSubscriptions } = await billing.check();
* // This will be true if any payment is found
* console.log(hasActivePayment);
* console.log(appSubscriptions);
* };
* ```
*/
check: <Options extends CheckBillingOptions<Config>>(
options?: Options,
) => Promise<BillingCheckResponseObject>;
/**
* Requests payment for the plan.
*
* @returns Redirects to the confirmation URL for the payment.
*
* @example
* <caption>Using a custom return URL.</caption>
* <description>Change where the merchant is returned to after approving the purchase using the `returnUrl` option.</description>
* ```ts
* // /app/routes/**\/*.ts
* import { LoaderFunctionArgs } from "@remix-run/node";
* import { authenticate, MONTHLY_PLAN } from "../shopify.server";
*
* export const loader = async ({ request }: LoaderFunctionArgs) => {
* const { billing } = await authenticate.admin(request);
* await billing.require({
* plans: [MONTHLY_PLAN],
* onFailure: async () => billing.request({
* plan: MONTHLY_PLAN,
* isTest: true,
* returnUrl: 'https://admin.shopify.com/store/my-store/apps/my-app/billing-page',
* }),
* });
*
* // App logic
* };
* ```
* ```ts
* // shopify.server.ts
* import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
*
* export const MONTHLY_PLAN = 'Monthly subscription';
* export const ANNUAL_PLAN = 'Annual subscription';
*
* const shopify = shopifyApp({
* // ...etc
* billing: {
* [MONTHLY_PLAN]: {
* lineItems: [
* {
* amount: 5,
* currencyCode: 'USD',
* interval: BillingInterval.Every30Days,
* }
* ],
* },
* [ANNUAL_PLAN]: {
* lineItems: [
* {
* amount: 50,
* currencyCode: 'USD',
* interval: BillingInterval.Annual,
* }
* ],
* },
* }
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*
* @example
* <caption>Overriding plan settings.</caption>
* <description>Customize the plan for a merchant when requesting billing. Any fields from the plan can be overridden, as long as the billing interval for line items matches the config.</description>
* ```ts
* // /app/routes/**\/*.ts
* import { LoaderFunctionArgs } from "@remix-run/node";
* import { authenticate, MONTHLY_PLAN } from "../shopify.server";
*
* export const loader = async ({ request }: LoaderFunctionArgs) => {
* const { billing } = await authenticate.admin(request);
* await billing.require({
* plans: [MONTHLY_PLAN],
* onFailure: async () => billing.request({
* plan: MONTHLY_PLAN,
* isTest: true,
* trialDays: 14,
* lineItems: [
* {
* interval: BillingInterval.Every30Days,
* discount: { value: { percentage: 0.1 } },
* },
* ],
* }),
* });
*
* // App logic
* };
* ```
* ```ts
* // shopify.server.ts
* import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
*
* export const MONTHLY_PLAN = 'Monthly subscription';
* export const ANNUAL_PLAN = 'Annual subscription';
*
* const shopify = shopifyApp({
* // ...etc
* billing: {
* [MONTHLY_PLAN]: {
* lineItems: [
* {
* amount: 5,
* currencyCode: 'USD',
* interval: BillingInterval.Every30Days,
* }
* ],
* },
* [ANNUAL_PLAN]: {
* lineItems: [
* {
* amount: 50,
* currencyCode: 'USD',
* interval: BillingInterval.Annual,
* }
* ],
* },
* }
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*/
request: (options: RequestBillingOptions<Config>) => Promise<never>;
/**
* Cancels an ongoing subscription, given its ID.
*
* @returns The cancelled subscription.
*
* @example
* <caption>Cancelling a subscription.</caption>
* <description>Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.</description>
* ```ts
* // /app/routes/cancel-subscription.ts
* import { LoaderFunctionArgs } from "@remix-run/node";
* import { authenticate, MONTHLY_PLAN } from "../shopify.server";
*
* export const loader = async ({ request }: LoaderFunctionArgs) => {
* const { billing } = await authenticate.admin(request);
* const billingCheck = await billing.require({
* plans: [MONTHLY_PLAN],
* onFailure: async () => billing.request({ plan: MONTHLY_PLAN }),
* });
*
* const subscription = billingCheck.appSubscriptions[0];
* const cancelledSubscription = await billing.cancel({
* subscriptionId: subscription.id,
* isTest: true,
* prorate: true,
* });
*
* // App logic
* };
* ```
* ```ts
* // shopify.server.ts
* import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
*
* export const MONTHLY_PLAN = 'Monthly subscription';
* export const ANNUAL_PLAN = 'Annual subscription';
*
* const shopify = shopifyApp({
* // ...etc
* billing: {
* [MONTHLY_PLAN]: {
* lineItems: [
* {
* amount: 5,
* currencyCode: 'USD',
* interval: BillingInterval.Every30Days,
* }
* ],
* },
* [ANNUAL_PLAN]: {
* lineItems: [
* {
* amount: 50,
* currencyCode: 'USD',
* interval: BillingInterval.Annual,
* }
* ],
* },
* }
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*/
cancel: (options: CancelBillingOptions) => Promise<AppSubscription>;
/**
* Creates a usage record for an app subscription.
*
* @returns Returns a usage record when one was created successfully.
*
* @example
* <caption>Creating a usage record</caption>
* <description>Create a usage record for the active usage billing plan</description>
* ```ts
* // /app/routes/create-usage-record.ts
* import { ActionFunctionArgs } from "@remix-run/node";
* import { authenticate, MONTHLY_PLAN } from "../shopify.server";
*
* export const action = async ({ request }: ActionFunctionArgs) => {
* const { billing } = await authenticate.admin(request);
*
* const chargeBilling = await billing.createUsageRecord({
* description: "Usage record for product creation",
* price: {
* amount: 1,
* currencyCode: "USD",
* },
* isTest: true,
* });
* console.log(chargeBilling);
*
* // App logic
* };
* ```
* ```ts
* // shopify.server.ts
* import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
*
* export const USAGE_PLAN = 'Usage subscription';
*
* const shopify = shopifyApp({
* // ...etc
* billing: {
* [USAGE_PLAN]: {
* lineItems: [
* {
* amount: 5,
* currencyCode: 'USD',
* interval: BillingInterval.Usage,
* }
* ],
* },
* }
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*/
createUsageRecord: (
options: CreateUsageRecordOptions,
) => Promise<UsageRecord>;
/**
* Updates the capped amount for a usage billing plan.
*
* @returns Redirects to a confirmation page to update the usage billing plan.
*
* @example
* <caption>Updating the capped amount for a usage billing plan.</caption>
* <description>Update the capped amount for the usage billing plan specified by `subscriptionLineItemId`.</description>
* ```ts
* // /app/routes/**\/*.ts
* import { ActionFunctionArgs } from "@remix-run/node";
* import { authenticate } from "../shopify.server";
*
* export const action = async ({ request }: ActionFunctionArgs) => {
* const { billing } = await authenticate.admin(request);
*
* await billing.updateUsageCappedAmount({
* subscriptionLineItemId: "gid://shopify/AppSubscriptionLineItem/12345?v=1&index=1",
* cappedAmount: {
* amount: 10,
* currencyCode: "USD"
* },
* });
*
* // App logic
* };
* ```
* ```ts
* // shopify.server.ts
* import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server";
*
* export const USAGE_PLAN = 'Usage subscription';
*
* const shopify = shopifyApp({
* // ...etc
* billing: {
* [USAGE_PLAN]: {
* lineItems: [
* {
* amount: 5,
* currencyCode: 'USD',
* interval: BillingInterval.Usage,
* terms: "Usage based"
* }
* ],
* },
* }
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*/
updateUsageCappedAmount: (
options: UpdateUsageCappedAmountOptions,
) => Promise<never>;
}
CancelBillingOptions
- isTest
Whether to use the test mode. This prevents the credit card from being charged.
boolean
- prorate
Whether to issue prorated credits for the unused portion of the app subscription. There will be a corresponding deduction (based on revenue share) to your Partner account. For example, if a $10.00 app subscription (with 0% revenue share) is cancelled and prorated half way through the billing cycle, then the merchant will be credited $5.00 and that amount will be deducted from your Partner account.
boolean
- subscriptionId
The ID of the subscription to cancel.
string
export interface CancelBillingOptions {
/**
* The ID of the subscription to cancel.
*/
subscriptionId: string;
/**
* Whether to issue prorated credits for the unused portion of the app subscription. There will be a corresponding
* deduction (based on revenue share) to your Partner account. For example, if a $10.00 app subscription
* (with 0% revenue share) is cancelled and prorated half way through the billing cycle, then the merchant will be
* credited $5.00 and that amount will be deducted from your Partner account.
*/
prorate?: boolean;
/**
* Whether to use the test mode. This prevents the credit card from being charged.
*/
isTest?: boolean;
}
CheckBillingOptions
- isTest
Whether to include charges that were created on test mode. Test shops and demo shops cannot be charged.
boolean
- plans
The plans to check for. Must be one of the values defined in the `billing` config option.
(keyof Config["billing"])[]
export interface CheckBillingOptions<Config extends AppConfigArg>
extends Omit<BillingCheckParams, 'session' | 'plans' | 'returnObject'> {
/**
* The plans to check for. Must be one of the values defined in the `billing` config option.
*/
plans?: (keyof Config['billing'])[];
}
CreateUsageRecordOptions
- description
The description of the app usage record.
string
- idempotencyKey
string
- isTest
Whether to use the test mode. This prevents the credit card from being charged.
boolean
- price
The price of the app usage record.
{ amount: number; currencyCode: string; }
- subscriptionLineItemId
string
export interface CreateUsageRecordOptions {
/**
* The description of the app usage record.
*/
description: string;
/**
* The price of the app usage record.
*/
price: {
/**
* The amount to charge for this usage record.
*/
amount: number;
/**
* The currency code for this usage record.
*/
currencyCode: string;
};
/**
* Whether to use the test mode. This prevents the credit card from being charged.
*/
isTest: boolean;
/*
* Defines the usage pricing plan the merchant is subscribed to.
*/
subscriptionLineItemId?: string;
/*
* A unique key generated to avoid duplicate charges.
*/
idempotencyKey?: string;
}
RequestBillingOptions
- isTest
Whether to use the test mode. This prevents the credit card from being charged. Test shops and demo shops cannot be charged.
boolean
- plan
The plan to request. Must be one of the values defined in the `billing` config option.
keyof Config["billing"]
- returnUrl
The URL to return to after the merchant approves the payment.
string
export interface RequestBillingOptions<Config extends AppConfigArg>
extends Omit<BillingRequestParams, 'session' | 'plan' | 'returnObject'> {
/**
* The plan to request. Must be one of the values defined in the `billing` config option.
*/
plan: keyof Config['billing'];
/**
* Whether to use the test mode. This prevents the credit card from being charged. Test shops and demo shops cannot be charged.
*/
isTest?: boolean;
/**
* The URL to return to after the merchant approves the payment.
*/
returnUrl?: string;
}
RequireBillingOptions
- isTest
Whether to include charges that were created on test mode. Test shops and demo shops cannot be charged.
boolean
- onFailure
How to handle the request if the shop doesn't have an active payment for any plan.
(error: any) => Promise<Response>
- plans
The plans to check for. Must be one of the values defined in the `billing` config option.
(keyof Config["billing"])[]
export interface RequireBillingOptions<Config extends AppConfigArg>
extends Omit<BillingCheckParams, 'session' | 'plans' | 'returnObject'> {
/**
* The plans to check for. Must be one of the values defined in the `billing` config option.
*/
plans: (keyof Config['billing'])[];
/**
* How to handle the request if the shop doesn't have an active payment for any plan.
*/
onFailure: (error: any) => Promise<Response>;
}
UpdateUsageCappedAmountOptions
- cappedAmount
The maximum charge for the usage billing plan.
{ amount: number; currencyCode: string; }
- subscriptionLineItemId
The subscription line item ID to update.
string
export interface UpdateUsageCappedAmountOptions {
/**
* The subscription line item ID to update.
*/
subscriptionLineItemId: string;
/**
* The maximum charge for the usage billing plan.
*/
cappedAmount: {
/**
* The amount to update.
*/
amount: number;
/**
* The currency code to update.
*/
currencyCode: string;
};
}
EnsureCORSFunction
export interface EnsureCORSFunction {
(response: Response): Response;
}
EmbeddedAdminContext
- admin
Methods for interacting with the GraphQL Admin API for the store that made the request.
AdminApiContext
- billing
Billing methods for this store, based on the plans defined in the `billing` config option.
BillingContext<Config>
- cors
A function that ensures the CORS headers are set correctly for the response.
EnsureCORSFunction
- redirect
A function that redirects the user to a new page, ensuring that the appropriate parameters are set for embedded apps. Returned only if `isEmbeddedApp` is `true`.
RedirectFunction
- session
The session for the user who made the request. This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice. Use this to get shop or user-specific data.
Session
- sessionToken
The decoded and validated session token for the request. Returned only if `isEmbeddedApp` is `true`.
JwtPayload
export interface EmbeddedAdminContext<Config extends AppConfigArg>
extends AdminContextInternal<Config> {
/**
* The decoded and validated session token for the request.
*
* Returned only if `isEmbeddedApp` is `true`.
*
* {@link https://shopify.dev/docs/apps/auth/oauth/session-tokens#payload}
*
* @example
* <caption>Using the decoded session token.</caption>
* <description>Get user-specific data using the `sessionToken` object.</description>
* ```ts
* // /app/routes/**\/*.ts
* import { LoaderFunctionArgs, json } from "@remix-run/node";
* import { authenticate } from "../shopify.server";
* import { getMyAppData } from "~/db/model.server";
*
* export const loader = async ({ request }: LoaderFunctionArgs) => {
* const { sessionToken } = await authenticate.admin(
* request
* );
* return json(await getMyAppData({user: sessionToken.sub}));
* };
* ```
* ```ts
* // shopify.server.ts
* import { shopifyApp } from "@shopify/shopify-app-remix/server";
*
* const shopify = shopifyApp({
* // ...etc
* useOnlineTokens: true,
* });
* export default shopify;
* export const authenticate = shopify.authenticate;
* ```
*/
sessionToken: JwtPayload;
/**
* A function that redirects the user to a new page, ensuring that the appropriate parameters are set for embedded
* apps.
*
* Returned only if `isEmbeddedApp` is `true`.
*
* @example
* <caption>Redirecting to an app route.</caption>
* <description>Use the `redirect` helper to safely redirect between pages.</description>
* ```ts
* // /app/routes/admin/my-route.ts
* import { LoaderFunctionArgs, json } from "@remix-run/node";
* import { authenticate } from "../shopify.server";
*
* export const loader = async ({ request }: LoaderFunctionArgs) => {
* const { session, redirect } = await authenticate.admin(request);
* return redirect("/");
* };
* ```
*
* @example
* <caption>Redirecting to a page in the Shopify Admin.</caption>
* <description>Redirects to a product page in the Shopify admin. Pass in a `target` option of `_top` or `_parent` to navigate in the current window, or `_blank` to open a new tab.</description>
* ```ts
* // /app/routes/admin/my-route.ts
* import { LoaderFunctionArgs, json } from "@remix-run/node";
* import { authenticate } from "../shopify.server";
*
* export const loader = async ({ request }: LoaderFunctionArgs) => {
* const { session, redirect } = await authenticate.admin(request);
* return redirect("shopify://admin/products/123456", { target: '_parent' });
* };
* ```
*
* @example
* <caption>Redirecting outside of the Admin embedded app page.</caption>
* <description>Pass in a `target` option of `_top` or `_parent` to navigate in the current window, or `_blank` to open a new tab.</description>
* ```ts
* // /app/routes/admin/my-route.ts
* import { LoaderFunctionArgs, json } from "@remix-run/node";
* import { authenticate } from "../shopify.server";
*
* export const loader = async ({ request }: LoaderFunctionArgs) => {
* const { session, redirect } = await authenticate.admin(request);
* return redirect("/", { target: '_parent' });
* };
* ```
*/
redirect: RedirectFunction;
}
RedirectFunction
- url
string
- init
RedirectInit
TypedResponse<never>
(
url: string,
init?: RedirectInit,
) => TypedResponse<never>
RedirectInit
number | (ResponseInit & {target?: RedirectTarget})
RedirectTarget
'_self' | '_parent' | '_top' | '_blank'
ScopesContext
- scopes
Methods to manage scopes for the store that made the request.
ScopesApiContext
export interface ScopesContext {
/**
* Methods to manage scopes for the store that made the request.
*/
scopes: ScopesApiContext;
}
ScopesApiContext
The Scopes API enables embedded apps and extensions to request merchant consent for access scopes.
- query
Queries Shopify to see what scopes have been granted
() => Promise<ScopesDetail>
- request
Requests the merchant grant the provided scopes This method performs a redirect to the grant screen.
(scopes: string[]) => Promise<void>
- revoke
Revokes the provided scopes Warning: This method throws an [error](https://shopify.dev/docs/api/admin-graphql/unstable/objects/AppRevokeAccessScopesAppRevokeScopeError) if the provided optional scopes contains a required scope.
(scopes: string[]) => Promise<ScopesRevokeResponse>
export interface ScopesApiContext {
/**
* Queries Shopify to see what scopes have been granted
*
* @returns {ScopesDetail} The scope details.
*
* @example
* <caption>Query for granted scopes.</caption>
* <description>Call `scopes.query` to get scope details.</description>
* ```ts
* // /app._index.tsx
* import { authenticate } from "../shopify.server";
* import { json } from "@remix-run/node";
*
* export const loader = async ({ request }) => {
* const { scopes } = await authenticate.admin(request);
* const { required, optional, granted } = await scopes.query();
*
* if (granted.includes("write_products")) {
* // do something
* }
*
* return null
* };
*
* ```
*/
query: () => Promise<ScopesDetail>;
/**
* Requests the merchant grant the provided scopes
*
* This method performs a redirect to the grant screen.
*
* @example
* <caption>Ask the merchant to grant the provided scopes.</caption>
* <description>Call `scopes.request` to request optional scopes.</description>
* ```ts
* // /app/routes/app.request.tsx
* import { authenticate } from "../shopify.server";
*
* // Example of an action to request optional scopes
* export const action = async ({ request }) => {
* const { scopes } = await authenticate.admin(request);
* const { granted } = await scopes.query();
*
* if (!granted.includes("write_products")) {
* await scopes.request(["write_products"]);
* }
*
* return null
* };
* ```
*/
request: (scopes: Scope[]) => Promise<void>;
/**
* Revokes the provided scopes
*
* Warning: This method throws an [error](https://shopify.dev/docs/api/admin-graphql/unstable/objects/AppRevokeAccessScopesAppRevokeScopeError) if the provided optional scopes contains a required scope.
*
* @example
* <caption>Revoke optional scopes.</caption>
* <description>Call `scopes.revoke` to revoke optional scopes.</description>
* ```ts
* // /app._index.tsx
* import { authenticate } from "../shopify.server";
* import { json } from "@remix-run/node";
*
* // Example of an action to POST optional scopes to revoke
* export const action = async ({ request }) => {
* const { scopes } = await authenticate.admin(request);
* const { granted } = await scopes.query();
*
* if (granted.includes("write_products")) {
* const revokedResponse = await scopes.revoke(["write_products"]);
* return json(revokedResponse);
* }
*
* return null
* };
*
* ```
*/
revoke: (scopes: Scope[]) => Promise<ScopesRevokeResponse>;
}
ScopesDetail
- granted
The scopes that have been granted on the shop for this app
string[]
- optional
The optional scopes that the app has declared in its configuration
string[]
- required
The required scopes that the app has declared in its configuration
string[]
export interface ScopesDetail {
/**
* The scopes that have been granted on the shop for this app
*/
granted: Scope[];
/**
* The required scopes that the app has declared in its configuration
*/
required: Scope[];
/**
* The optional scopes that the app has declared in its configuration
*/
optional: Scope[];
}
ScopesRevokeResponse
- revoked
The scopes that have been revoked on the shop for this app
string[]
export interface ScopesRevokeResponse {
/**
* The scopes that have been revoked on the shop for this app
*/
revoked: Scope[];
}
Authenticate, run API mutation, and redirect
/app/routes/**.ts
Examples
Authenticate, run API mutation, and redirect
Description
Authenticate, run API mutation, and redirect
/app/routes/**.ts
import {type ActionFunctionArgs, json} from '@remix-run/node'; import {GraphqlQueryError} from '@shopify/shopify-api'; import {authenticate} from '../shopify.server'; export const action = async ({request}: ActionFunctionArgs) => { const {admin, redirect} = await authenticate.admin(request); try { await admin.graphql( `#graphql mutation updateProductTitle($input: ProductInput!) { productUpdate(input: $input) { product { id } } }`, { variables: { input: {id: '123', title: 'New title'}, }, }, ); return redirect('/app/product-updated'); } catch (error) { if (error instanceof GraphqlQueryError) { return json({errors: error.body?.errors}, {status: 500}); } return new Response('Failed to update product title', {status: 500}); } };
Anchor to examplesExamples
Anchor to example-setting-cors-headers-for-a-admin-requestSetting CORS headers for a admin request
Use the cors
helper to ensure your app can respond to requests from admin extensions.
Setting CORS headers for a admin request
/app/routes/admin/my-route.ts
Examples
Setting CORS headers for a admin request
Description
Use the `cors` helper to ensure your app can respond to requests from admin extensions.
/app/routes/admin/my-route.ts
import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import { getMyAppData } from "~/db/model.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session, cors } = await authenticate.admin(request); return cors(json(await getMyAppData({user: session.onlineAccessInfo!.id}))); };
Anchor to example-redirectredirect
Anchor to example-redirecting-to-an-app-routeRedirecting to an app route
Use the redirect
helper to safely redirect between pages.
Anchor to example-redirecting-to-a-page-in-the-shopify-adminRedirecting to a page in the Shopify Admin
Redirects to a product page in the Shopify admin. Pass in a target
option of _top
or _parent
to navigate in the current window, or _blank
to open a new tab.
Anchor to example-redirecting-outside-of-the-admin-embedded-app-pageRedirecting outside of the Admin embedded app page
Pass in a target
option of _top
or _parent
to navigate in the current window, or _blank
to open a new tab.
Redirecting to an app route
/app/routes/admin/my-route.ts
Examples
Redirecting to an app route
Description
Use the `redirect` helper to safely redirect between pages.
/app/routes/admin/my-route.ts
import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session, redirect } = await authenticate.admin(request); return redirect("/"); };
Redirecting to a page in the Shopify Admin
Description
Redirects to a product page in the Shopify admin. Pass in a `target` option of `_top` or `_parent` to navigate in the current window, or `_blank` to open a new tab.
/app/routes/admin/my-route.ts
import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session, redirect } = await authenticate.admin(request); return redirect("shopify://admin/products/123456", { target: '_parent' }); };
Redirecting outside of the Admin embedded app page
Description
Pass in a `target` option of `_top` or `_parent` to navigate in the current window, or `_blank` to open a new tab.
/app/routes/admin/my-route.ts
import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session, redirect } = await authenticate.admin(request); return redirect("/", { target: '_parent' }); };
Anchor to example-sessionsession
Anchor to example-using-offline-sessionsUsing offline sessions
Get your app's shop-specific data using an offline session.
Anchor to example-using-online-sessionsUsing online sessions
Get your app's user-specific data using an online session.
Using offline sessions
Examples
Using offline sessions
Description
Get your app's shop-specific data using an offline session.
/app/routes/**\/*.ts
import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import { getMyAppData } from "~/db/model.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session } = await authenticate.admin(request); return json(await getMyAppData({shop: session.shop)); };
shopify.server.ts
import { shopifyApp } from "@shopify/shopify-app-remix/server"; const shopify = shopifyApp({ // ...etc }); export default shopify; export const authenticate = shopify.authenticate;
Using online sessions
Description
Get your app's user-specific data using an online session.
/app/routes/**\/*.ts
import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import { getMyAppData } from "~/db/model.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session } = await authenticate.admin(request); return json(await getMyAppData({user: session.onlineAccessInfo!.id})); };
shopify.server.ts
import { shopifyApp } from "@shopify/shopify-app-remix/server"; const shopify = shopifyApp({ // ...etc useOnlineTokens: true, }); export default shopify; export const authenticate = shopify.authenticate;
Anchor to example-sessiontokensessionToken
Anchor to example-using-the-decoded-session-tokenUsing the decoded session token
Get user-specific data using the object.
Using the decoded session token
Examples
Using the decoded session token
Description
Get user-specific data using the `sessionToken` object.
/app/routes/**\/*.ts
import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import { getMyAppData } from "~/db/model.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { sessionToken } = await authenticate.admin( request ); return json(await getMyAppData({user: sessionToken.sub})); };
shopify.server.ts
import { shopifyApp } from "@shopify/shopify-app-remix/server"; const shopify = shopifyApp({ // ...etc useOnlineTokens: true, }); export default shopify; export const authenticate = shopify.authenticate;
Anchor to example-graphqlgraphql
Anchor to example-querying-the-graphql-apiQuerying the GraphQL API
Use admin.graphql
to make query / mutation requests.
Anchor to example-handling-graphql-errorsHandling GraphQL errors
Catch errors to see error messages from the API.
Querying the GraphQL API
Examples
Querying the GraphQL API
Description
Use `admin.graphql` to make query / mutation requests.
/app/routes/**\/*.ts
import { ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const action = async ({ request }: ActionFunctionArgs) => { const { admin } = await authenticate.admin(request); const response = await admin.graphql( `#graphql mutation populateProduct($input: ProductInput!) { productCreate(input: $input) { product { id } } }`, { variables: { input: { title: "Product Name" }, }, }, ); const productData = await response.json(); return json({ productId: productData.data?.productCreate?.product?.id, }); }
/app/shopify.server.ts
import { shopifyApp } from "@shopify/shopify-app-remix/server"; const shopify = shopifyApp({ // ... }); export default shopify; export const authenticate = shopify.authenticate;
Handling GraphQL errors
Description
Catch `GraphqlQueryError` errors to see error messages from the API.
/app/routes/**\/*.ts
import { ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const action = async ({ request }: ActionFunctionArgs) => { const { admin } = await authenticate.admin(request); try { const response = await admin.graphql( `#graphql query incorrectQuery { products(first: 10) { nodes { not_a_field } } }`, ); return json({ data: await response.json() }); } catch (error) { if (error instanceof GraphqlQueryError) { // error.body.errors: // { graphQLErrors: [ // { message: "Field 'not_a_field' doesn't exist on type 'Product'" } // ] } return json({ errors: error.body?.errors }, { status: 500 }); } return json({ message: "An error occurred" }, { status: 500 }); } }
/app/shopify.server.ts
import { shopifyApp } from "@shopify/shopify-app-remix/server"; const shopify = shopifyApp({ // ... }); export default shopify; export const authenticate = shopify.authenticate;
Anchor to example-cancelcancel
Anchor to example-cancelling-a-subscriptionCancelling a subscription
Use the billing.cancel
function to cancel an active subscription with the id returned from billing.require
.
Cancelling a subscription
Examples
Cancelling a subscription
Description
Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`.
/app/routes/cancel-subscription.ts
import { LoaderFunctionArgs } from "@remix-run/node"; import { authenticate, MONTHLY_PLAN } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { billing } = await authenticate.admin(request); const billingCheck = await billing.require({ plans: [MONTHLY_PLAN], onFailure: async () => billing.request({ plan: MONTHLY_PLAN }), }); const subscription = billingCheck.appSubscriptions[0]; const cancelledSubscription = await billing.cancel({ subscriptionId: subscription.id, isTest: true, prorate: true, }); // App logic };
shopify.server.ts
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server"; export const MONTHLY_PLAN = 'Monthly subscription'; export const ANNUAL_PLAN = 'Annual subscription'; const shopify = shopifyApp({ // ...etc billing: { [MONTHLY_PLAN]: { lineItems: [ { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, } ], }, [ANNUAL_PLAN]: { lineItems: [ { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, } ], }, } }); export default shopify; export const authenticate = shopify.authenticate;
Anchor to example-check-what-billing-plans-a-merchant-is-subscribed-toCheck what billing plans a merchant is subscribed to
Use billing.check if you want to determine which plans are in use. Unlike require
, check
does notthrow an error if no active billing plans are present.
Anchor to example-check-for-payments-without-filteringCheck for payments without filtering
Use billing.check to see if any payments exist for the store, regardless of whether it's a test ormatches one or more plans.
Check what billing plans a merchant is subscribed to
Examples
Check what billing plans a merchant is subscribed to
Description
Use billing.check if you want to determine which plans are in use. Unlike `require`, `check` does notthrow an error if no active billing plans are present.
/app/routes/**\/*.ts
import { LoaderFunctionArgs } from "@remix-run/node"; import { authenticate, MONTHLY_PLAN } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { billing } = await authenticate.admin(request); const { hasActivePayment, appSubscriptions } = await billing.check({ plans: [MONTHLY_PLAN], isTest: false, }); console.log(hasActivePayment); console.log(appSubscriptions); };
shopify.server.ts
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server"; export const MONTHLY_PLAN = 'Monthly subscription'; export const ANNUAL_PLAN = 'Annual subscription'; const shopify = shopifyApp({ // ...etc billing: { [MONTHLY_PLAN]: { lineItems: [ { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, } ], }, [ANNUAL_PLAN]: { lineItems: [ { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, } ], }, } }); export default shopify; export const authenticate = shopify.authenticate;
Check for payments without filtering
Description
Use billing.check to see if any payments exist for the store, regardless of whether it's a test ormatches one or more plans.
/app/routes/**\/*.ts
import { LoaderFunctionArgs } from "@remix-run/node"; import { authenticate, MONTHLY_PLAN } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { billing } = await authenticate.admin(request); const { hasActivePayment, appSubscriptions } = await billing.check(); // This will be true if any payment is found console.log(hasActivePayment); console.log(appSubscriptions); };
Anchor to example-createusagerecordcreateUsageRecord
Anchor to example-creating-a-usage-recordCreating a usage record
Create a usage record for the active usage billing plan
Creating a usage record
Examples
Creating a usage record
Description
Create a usage record for the active usage billing plan
/app/routes/create-usage-record.ts
import { ActionFunctionArgs } from "@remix-run/node"; import { authenticate, MONTHLY_PLAN } from "../shopify.server"; export const action = async ({ request }: ActionFunctionArgs) => { const { billing } = await authenticate.admin(request); const chargeBilling = await billing.createUsageRecord({ description: "Usage record for product creation", price: { amount: 1, currencyCode: "USD", }, isTest: true, }); console.log(chargeBilling); // App logic };
shopify.server.ts
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server"; export const USAGE_PLAN = 'Usage subscription'; const shopify = shopifyApp({ // ...etc billing: { [USAGE_PLAN]: { lineItems: [ { amount: 5, currencyCode: 'USD', interval: BillingInterval.Usage, } ], }, } }); export default shopify; export const authenticate = shopify.authenticate;
Anchor to example-requestrequest
Anchor to example-using-a-custom-return-urlUsing a custom return URL
Change where the merchant is returned to after approving the purchase using the option.
Anchor to example-overriding-plan-settingsOverriding plan settings
Customize the plan for a merchant when requesting billing. Any fields from the plan can be overridden, as long as the billing interval for line items matches the config.
Using a custom return URL
Examples
Using a custom return URL
Description
Change where the merchant is returned to after approving the purchase using the `returnUrl` option.
/app/routes/**\/*.ts
import { LoaderFunctionArgs } from "@remix-run/node"; import { authenticate, MONTHLY_PLAN } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { billing } = await authenticate.admin(request); await billing.require({ plans: [MONTHLY_PLAN], onFailure: async () => billing.request({ plan: MONTHLY_PLAN, isTest: true, returnUrl: 'https://admin.shopify.com/store/my-store/apps/my-app/billing-page', }), }); // App logic };
shopify.server.ts
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server"; export const MONTHLY_PLAN = 'Monthly subscription'; export const ANNUAL_PLAN = 'Annual subscription'; const shopify = shopifyApp({ // ...etc billing: { [MONTHLY_PLAN]: { lineItems: [ { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, } ], }, [ANNUAL_PLAN]: { lineItems: [ { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, } ], }, } }); export default shopify; export const authenticate = shopify.authenticate;
Overriding plan settings
Description
Customize the plan for a merchant when requesting billing. Any fields from the plan can be overridden, as long as the billing interval for line items matches the config.
/app/routes/**\/*.ts
import { LoaderFunctionArgs } from "@remix-run/node"; import { authenticate, MONTHLY_PLAN } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { billing } = await authenticate.admin(request); await billing.require({ plans: [MONTHLY_PLAN], onFailure: async () => billing.request({ plan: MONTHLY_PLAN, isTest: true, trialDays: 14, lineItems: [ { interval: BillingInterval.Every30Days, discount: { value: { percentage: 0.1 } }, }, ], }), }); // App logic };
shopify.server.ts
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server"; export const MONTHLY_PLAN = 'Monthly subscription'; export const ANNUAL_PLAN = 'Annual subscription'; const shopify = shopifyApp({ // ...etc billing: { [MONTHLY_PLAN]: { lineItems: [ { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, } ], }, [ANNUAL_PLAN]: { lineItems: [ { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, } ], }, } }); export default shopify; export const authenticate = shopify.authenticate;
Anchor to example-requirerequire
Anchor to example-requesting-billing-right-awayRequesting billing right away
Call billing.request
in the callback to immediately redirect to the Shopify page to request payment.
Anchor to example-redirect-to-a-plan-selection-pageRedirect to a plan selection page
When the app has multiple plans, create a page in your App that allows the merchant to select a plan. If a merchant does not have the required plan you can redirect them to page in your app to select one.
Requesting billing right away
Examples
Requesting billing right away
Description
Call `billing.request` in the `onFailure` callback to immediately redirect to the Shopify page to request payment.
/app/routes/**\/*.ts
import { LoaderFunctionArgs } from "@remix-run/node"; import { authenticate, MONTHLY_PLAN } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { billing } = await authenticate.admin(request); await billing.require({ plans: [MONTHLY_PLAN], isTest: true, onFailure: async () => billing.request({ plan: MONTHLY_PLAN }), }); // App logic };
shopify.server.ts
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server"; export const MONTHLY_PLAN = 'Monthly subscription'; const shopify = shopifyApp({ // ...etc billing: { [MONTHLY_PLAN]: { lineItems: [ { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, } ], }, } }); export default shopify; export const authenticate = shopify.authenticate;
Redirect to a plan selection page
Description
When the app has multiple plans, create a page in your App that allows the merchant to select a plan. If a merchant does not have the required plan you can redirect them to page in your app to select one.
/app/routes/**\/*.ts
import { LoaderFunctionArgs, redirect } from "@remix-run/node"; import { authenticate, MONTHLY_PLAN, ANNUAL_PLAN } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { billing } = await authenticate.admin(request); const billingCheck = await billing.require({ plans: [MONTHLY_PLAN, ANNUAL_PLAN], isTest: true, onFailure: () => redirect('/select-plan'), }); const subscription = billingCheck.appSubscriptions[0]; console.log(`Shop is on ${subscription.name} (id ${subscription.id})`); // App logic };
shopify.server.ts
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server"; export const MONTHLY_PLAN = 'Monthly subscription'; export const ANNUAL_PLAN = 'Annual subscription'; const shopify = shopifyApp({ // ...etc billing: { [MONTHLY_PLAN]: { lineItems: [ { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, } ], }, [ANNUAL_PLAN]: { lineItems: [ { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, } ], }, } }); export default shopify; export const authenticate = shopify.authenticate;
Anchor to example-updateusagecappedamountupdateUsageCappedAmount
Anchor to example-updating-the-capped-amount-for-a-usage-billing-planUpdating the capped amount for a usage billing plan
Update the capped amount for the usage billing plan specified by .
Updating the capped amount for a usage billing plan
Examples
Updating the capped amount for a usage billing plan
Description
Update the capped amount for the usage billing plan specified by `subscriptionLineItemId`.
/app/routes/**\/*.ts
import { ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const action = async ({ request }: ActionFunctionArgs) => { const { billing } = await authenticate.admin(request); await billing.updateUsageCappedAmount({ subscriptionLineItemId: "gid://shopify/AppSubscriptionLineItem/12345?v=1&index=1", cappedAmount: { amount: 10, currencyCode: "USD" }, }); // App logic };
shopify.server.ts
import { shopifyApp, BillingInterval } from "@shopify/shopify-app-remix/server"; export const USAGE_PLAN = 'Usage subscription'; const shopify = shopifyApp({ // ...etc billing: { [USAGE_PLAN]: { lineItems: [ { amount: 5, currencyCode: 'USD', interval: BillingInterval.Usage, terms: "Usage based" } ], }, } }); export default shopify; export const authenticate = shopify.authenticate;
Anchor to example-query-for-granted-scopesQuery for granted scopes
Call scopes.query
to get scope details.
Query for granted scopes
/app._index.tsx
Examples
Query for granted scopes
Description
Call `scopes.query` to get scope details.
/app._index.tsx
import { authenticate } from "../shopify.server"; import { json } from "@remix-run/node"; export const loader = async ({ request }) => { const { scopes } = await authenticate.admin(request); const { required, optional, granted } = await scopes.query(); if (granted.includes("write_products")) { // do something } return null };
Anchor to example-requestrequest
Anchor to example-ask-the-merchant-to-grant-the-provided-scopesAsk the merchant to grant the provided scopes
Call scopes.request
to request optional scopes.
Ask the merchant to grant the provided scopes
/app/routes/app.request.tsx
Examples
Ask the merchant to grant the provided scopes
Description
Call `scopes.request` to request optional scopes.
/app/routes/app.request.tsx
import { authenticate } from "../shopify.server"; // Example of an action to request optional scopes export const action = async ({ request }) => { const { scopes } = await authenticate.admin(request); const { granted } = await scopes.query(); if (!granted.includes("write_products")) { await scopes.request(["write_products"]); } return null };
Anchor to example-revokerevoke
Anchor to example-revoke-optional-scopesRevoke optional scopes
Call scopes.revoke
to revoke optional scopes.
Revoke optional scopes
/app._index.tsx
Examples
Revoke optional scopes
Description
Call `scopes.revoke` to revoke optional scopes.
/app._index.tsx
import { authenticate } from "../shopify.server"; import { json } from "@remix-run/node"; // Example of an action to POST optional scopes to revoke export const action = async ({ request }) => { const { scopes } = await authenticate.admin(request); const { granted } = await scopes.query(); if (granted.includes("write_products")) { const revokedResponse = await scopes.revoke(["write_products"]); return json(revokedResponse); } return null };