--- title: Admin description: >- Contains functions 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. api_version: v2 api_name: shopify-app-remix source_url: html: 'https://shopify.dev/docs/api/shopify-app-remix/v2/authenticate/admin' md: 'https://shopify.dev/docs/api/shopify-app-remix/v2/authenticate/admin.md' --- # Admin Contains functions 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. ## authenticate.​admin([request](#authenticateadmin-propertydetail-request)​) Authenticates requests coming from the Shopify admin. The shape of the returned object changes depending on the `isEmbeddedApp` config. ### Parameters * request Request required ### Returns * Promise\> ### AdminContext ```ts Config['isEmbeddedApp'] extends false ? NonEmbeddedAdminContext : EmbeddedAdminContext ``` ### NonEmbeddedAdminContext * 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. ```ts Session ``` * admin Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request. ```ts AdminApiContext ``` * billing Billing methods for this store, based on the plans defined in the \`billing\` config option. ```ts BillingContext ``` * cors A function that ensures the CORS headers are set correctly for the response. ```ts EnsureCORSFunction ``` ```ts export interface NonEmbeddedAdminContext< Config extends AppConfigArg, Resources extends ShopifyRestResources = ShopifyRestResources, > extends AdminContextInternal {} ``` ### Session Stores App information from logged in merchants so they can make authenticated requests to the Admin API. * id The unique identifier for the session. ```ts string ``` * shop The Shopify shop domain, such as \`example.myshopify.com\`. ```ts string ``` * state The state of the session. Used for the OAuth authentication code flow. ```ts string ``` * isOnline Whether the access token in the session is online or offline. ```ts boolean ``` * scope The desired scopes for the access token, at the time the session was created. ```ts string ``` * expires The date the access token expires. ```ts Date ``` * accessToken The access token for the session. ```ts string ``` * onlineAccessInfo Information on the user for the session. Only present for online sessions. ```ts OnlineAccessInfo ``` * isActive Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes. ```ts (scopes: string | string[] | AuthScopes) => boolean ``` * isScopeChanged Whether the access token has the given scopes. ```ts (scopes: string | string[] | AuthScopes) => boolean ``` * isExpired Whether the access token is expired. ```ts (withinMillisecondsOfExpiry?: number) => boolean ``` * toObject Converts an object with data into a Session. ```ts () => SessionParams ``` * equals Checks whether the given session is equal to this session. ```ts (other: Session) => boolean ``` * toPropertyArray Converts the session into an array of key-value pairs. ```ts (returnUserData?: boolean) => [string, string | number | boolean][] ``` ```ts export class Session { public static fromPropertyArray( entries: [string, string | number | boolean][], returnUserData = false, ): Session { if (!Array.isArray(entries)) { throw new InvalidSession( 'The parameter is not an array: a Session cannot be created from this object.', ); } const obj = Object.fromEntries( entries .filter(([_key, value]) => value !== null && value !== undefined) // Sanitize keys .map(([key, value]) => { switch (key.toLowerCase()) { case 'isonline': return ['isOnline', value]; case 'accesstoken': return ['accessToken', value]; case 'onlineaccessinfo': return ['onlineAccessInfo', value]; case 'userid': return ['userId', value]; case 'firstname': return ['firstName', value]; case 'lastname': return ['lastName', value]; case 'accountowner': return ['accountOwner', value]; case 'emailverified': return ['emailVerified', value]; default: return [key.toLowerCase(), value]; } }), ); const sessionData = {} as SessionParams; const onlineAccessInfo = { associated_user: {}, } as OnlineAccessInfo; Object.entries(obj).forEach(([key, value]) => { switch (key) { case 'isOnline': if (typeof value === 'string') { sessionData[key] = value.toString().toLowerCase() === 'true'; } else if (typeof value === 'number') { sessionData[key] = Boolean(value); } else { sessionData[key] = value; } break; case 'scope': sessionData[key] = value.toString(); break; case 'expires': sessionData[key] = value ? new Date(Number(value)) : undefined; break; case 'onlineAccessInfo': onlineAccessInfo.associated_user.id = Number(value); break; case 'userId': if (returnUserData) { onlineAccessInfo.associated_user.id = Number(value); break; } case 'firstName': if (returnUserData) { onlineAccessInfo.associated_user.first_name = String(value); break; } case 'lastName': if (returnUserData) { onlineAccessInfo.associated_user.last_name = String(value); break; } case 'email': if (returnUserData) { onlineAccessInfo.associated_user.email = String(value); break; } case 'accountOwner': if (returnUserData) { onlineAccessInfo.associated_user.account_owner = Boolean(value); break; } case 'locale': if (returnUserData) { onlineAccessInfo.associated_user.locale = String(value); break; } case 'collaborator': if (returnUserData) { onlineAccessInfo.associated_user.collaborator = Boolean(value); break; } case 'emailVerified': if (returnUserData) { onlineAccessInfo.associated_user.email_verified = Boolean(value); break; } // Return any user keys as passed in default: sessionData[key] = value; } }); if (sessionData.isOnline) { sessionData.onlineAccessInfo = onlineAccessInfo; } const session = new Session(sessionData); return session; } /** * The unique identifier for the session. */ readonly id: string; /** * The Shopify shop domain, such as `example.myshopify.com`. */ public shop: string; /** * The state of the session. Used for the OAuth authentication code flow. */ public state: string; /** * Whether the access token in the session is online or offline. */ public isOnline: boolean; /** * The desired scopes for the access token, at the time the session was created. */ public scope?: string; /** * The date the access token expires. */ public expires?: Date; /** * The access token for the session. */ public accessToken?: string; /** * Information on the user for the session. Only present for online sessions. */ public onlineAccessInfo?: OnlineAccessInfo; constructor(params: SessionParams) { Object.assign(this, params); } /** * Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes. */ public isActive(scopes: AuthScopes | string | string[]): boolean { return ( !this.isScopeChanged(scopes) && Boolean(this.accessToken) && !this.isExpired() ); } /** * Whether the access token has the given scopes. */ public isScopeChanged(scopes: AuthScopes | string | string[]): boolean { const scopesObject = scopes instanceof AuthScopes ? scopes : new AuthScopes(scopes); return !scopesObject.equals(this.scope); } /** * Whether the access token is expired. */ public isExpired(withinMillisecondsOfExpiry = 0): boolean { return Boolean( this.expires && this.expires.getTime() - withinMillisecondsOfExpiry < Date.now(), ); } /** * Converts an object with data into a Session. */ public toObject(): SessionParams { const object: SessionParams = { id: this.id, shop: this.shop, state: this.state, isOnline: this.isOnline, }; if (this.scope) { object.scope = this.scope; } if (this.expires) { object.expires = this.expires; } if (this.accessToken) { object.accessToken = this.accessToken; } if (this.onlineAccessInfo) { object.onlineAccessInfo = this.onlineAccessInfo; } return object; } /** * Checks whether the given session is equal to this session. */ public equals(other: Session | undefined): boolean { if (!other) return false; const mandatoryPropsMatch = this.id === other.id && this.shop === other.shop && this.state === other.state && this.isOnline === other.isOnline; if (!mandatoryPropsMatch) return false; const copyA = this.toPropertyArray(true); copyA.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); const copyB = other.toPropertyArray(true); copyB.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); return JSON.stringify(copyA) === JSON.stringify(copyB); } /** * Converts the session into an array of key-value pairs. */ public toPropertyArray( returnUserData = false, ): [string, string | number | boolean][] { return ( Object.entries(this) .filter( ([key, value]) => propertiesToSave.includes(key) && value !== undefined && value !== null, ) // Prepare values for db storage .flatMap(([key, value]): [string, string | number | boolean][] => { switch (key) { case 'expires': return [[key, value ? value.getTime() : undefined]]; case 'onlineAccessInfo': // eslint-disable-next-line no-negated-condition if (!returnUserData) { return [[key, value.associated_user.id]]; } else { return [ ['userId', value?.associated_user?.id], ['firstName', value?.associated_user?.first_name], ['lastName', value?.associated_user?.last_name], ['email', value?.associated_user?.email], ['locale', value?.associated_user?.locale], ['emailVerified', value?.associated_user?.email_verified], ['accountOwner', value?.associated_user?.account_owner], ['collaborator', value?.associated_user?.collaborator], ]; } default: return [[key, value]]; } }) // Filter out tuples with undefined values .filter(([_key, value]) => value !== undefined) ); } } ``` ### OnlineAccessInfo * expires\_in How long the access token is valid for, in seconds. ```ts number ``` * associated\_user\_scope The effective set of scopes for the session. ```ts string ``` * associated\_user The user associated with the access token. ```ts OnlineAccessUser ``` ```ts export interface OnlineAccessInfo { /** * How long the access token is valid for, in seconds. */ expires_in: number; /** * The effective set of scopes for the session. */ associated_user_scope: string; /** * The user associated with the access token. */ associated_user: OnlineAccessUser; } ``` ### OnlineAccessUser * id The user's ID. ```ts number ``` * first\_name The user's first name. ```ts string ``` * last\_name The user's last name. ```ts string ``` * email The user's email address. ```ts string ``` * email\_verified Whether the user has verified their email address. ```ts boolean ``` * account\_owner Whether the user is the account owner. ```ts boolean ``` * locale The user's locale. ```ts string ``` * collaborator Whether the user is a collaborator. ```ts boolean ``` ```ts export interface OnlineAccessUser { /** * The user's ID. */ id: number; /** * The user's first name. */ first_name: string; /** * The user's last name. */ last_name: string; /** * The user's email address. */ email: string; /** * Whether the user has verified their email address. */ email_verified: boolean; /** * Whether the user is the account owner. */ account_owner: boolean; /** * The user's locale. */ locale: string; /** * Whether the user is a collaborator. */ collaborator: boolean; } ``` ### AuthScopes A class that represents a set of access token scopes. * has Checks whether the current set of scopes includes the given one. ```ts (scope: string | string[] | AuthScopes) => boolean ``` * equals Checks whether the current set of scopes equals the given one. ```ts (otherScopes: string | string[] | AuthScopes) => boolean ``` * toString Returns a comma-separated string with the current set of scopes. ```ts () => string ``` * toArray Returns an array with the current set of scopes. ```ts () => any[] ``` ```ts class AuthScopes { public static SCOPE_DELIMITER = ','; private compressedScopes: Set; private expandedScopes: Set; constructor(scopes: string | string[] | AuthScopes | undefined) { let scopesArray: string[] = []; if (typeof scopes === 'string') { scopesArray = scopes.split( new RegExp(`${AuthScopes.SCOPE_DELIMITER}\\s*`), ); } else if (Array.isArray(scopes)) { scopesArray = scopes; } else if (scopes) { scopesArray = Array.from(scopes.expandedScopes); } scopesArray = scopesArray .map((scope) => scope.trim()) .filter((scope) => scope.length); const impliedScopes = this.getImpliedScopes(scopesArray); const scopeSet = new Set(scopesArray); const impliedSet = new Set(impliedScopes); this.compressedScopes = new Set( [...scopeSet].filter((x) => !impliedSet.has(x)), ); this.expandedScopes = new Set([...scopeSet, ...impliedSet]); } /** * Checks whether the current set of scopes includes the given one. */ public has(scope: string | string[] | AuthScopes | undefined) { let other: AuthScopes; if (scope instanceof AuthScopes) { other = scope; } else { other = new AuthScopes(scope); } return ( other.toArray().filter((x) => !this.expandedScopes.has(x)).length === 0 ); } /** * Checks whether the current set of scopes equals the given one. */ public equals(otherScopes: string | string[] | AuthScopes | undefined) { let other: AuthScopes; if (otherScopes instanceof AuthScopes) { other = otherScopes; } else { other = new AuthScopes(otherScopes); } return ( this.compressedScopes.size === other.compressedScopes.size && this.toArray().filter((x) => !other.has(x)).length === 0 ); } /** * Returns a comma-separated string with the current set of scopes. */ public toString() { return this.toArray().join(AuthScopes.SCOPE_DELIMITER); } /** * Returns an array with the current set of scopes. */ public toArray() { return [...this.compressedScopes]; } private getImpliedScopes(scopesArray: string[]): string[] { return scopesArray.reduce((array: string[], current: string) => { const matches = current.match(/^(unauthenticated_)?write_(.*)$/); if (matches) { array.push(`${matches[1] ? matches[1] : ''}read_${matches[2]}`); } return array; }, []); } } ``` ### SessionParams * \[key: string] ```ts any ``` * id The unique identifier for the session. ```ts string ``` * shop The Shopify shop domain. ```ts string ``` * state The state of the session. Used for the OAuth authentication code flow. ```ts string ``` * isOnline Whether the access token in the session is online or offline. ```ts boolean ``` * scope The scopes for the access token. ```ts string ``` * expires The date the access token expires. ```ts Date ``` * accessToken The access token for the session. ```ts string ``` * onlineAccessInfo Information on the user for the session. Only present for online sessions. ```ts OnlineAccessInfo | StoredOnlineAccessInfo ``` ```ts export interface SessionParams { /** * The unique identifier for the session. */ readonly id: string; /** * The Shopify shop domain. */ shop: string; /** * The state of the session. Used for the OAuth authentication code flow. */ state: string; /** * Whether the access token in the session is online or offline. */ isOnline: boolean; /** * The scopes for the access token. */ scope?: string; /** * The date the access token expires. */ expires?: Date; /** * The access token for the session. */ accessToken?: string; /** * Information on the user for the session. Only present for online sessions. */ onlineAccessInfo?: OnlineAccessInfo | StoredOnlineAccessInfo; /** * Additional properties of the session allowing for extension */ [key: string]: any; } ``` ### StoredOnlineAccessInfo ```ts Omit & { associated_user: Partial; } ``` ### AdminApiContext * rest Methods for interacting with the Shopify Admin REST API There are methods for interacting with individual REST resources. You can also make \`GET\`, \`POST\`, \`PUT\` and \`DELETE\` requests should the REST resources not meet your needs. ```ts RestClientWithResources ``` * graphql Methods for interacting with the Shopify Admin GraphQL API ```ts GraphQLClient ``` ````ts export interface AdminApiContext< Resources extends ShopifyRestResources = ShopifyRestResources, > { /** * Methods for interacting with the Shopify Admin REST API * * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs. * * {@link https://shopify.dev/docs/api/admin-rest} * * @example * Using REST resources. * Getting the number of orders in a store using REST resources. Visit the [Admin REST API references](/docs/api/admin-rest) for examples on using each resource. * * ```ts * // /app/routes/**\/*.ts * import { LoaderFunctionArgs, json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export const loader = async ({ request }: LoaderFunctionArgs) => { * const { * admin, * session, * } = await authenticate.admin(request); * * return json( * admin.rest.resources.Order.count({ session }), * ); * }; * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * import { restResources } from "@shopify/shopify-api/rest/admin/2023-07"; * * const shopify = shopifyApp({ * restResources, * // ...etc * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` * * @example * Performing a GET request to the REST API. * Use `admin.rest.get` to make custom requests to make a request to to the `customer/count` endpoint * * ```ts * // /app/routes/**\/*.ts * import { LoaderFunctionArgs, json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export const loader = async ({ request }: LoaderFunctionArgs) => { * const { * admin, * session, * } = await authenticate.admin(request); * * const response = await admin.rest.get({ * path: "/customers/count.json", * }); * const customers = await response.json(); * * return json({ customers }); * }; * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; * * const shopify = shopifyApp({ * restResources, * // ...etc * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` * * @example * Performing a POST request to the REST API. * Use `admin.rest.post` to make custom requests to make a request to to the `customers.json` endpoint to send a welcome email * ```ts * // /app/routes/**\/*.ts * import { LoaderFunctionArgs, json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export const loader = async ({ request }: LoaderFunctionArgs) => { * const { * admin, * session, * } = await authenticate.admin(request); * * const response = admin.rest.post({ * path: "customers/7392136888625/send_invite.json", * body: { * customer_invite: { * to: "new_test_email@shopify.com", * from: "j.limited@example.com", * bcc: ["j.limited@example.com"], * subject: "Welcome to my new shop", * custom_message: "My awesome new store", * }, * }, * }); * * const customerInvite = await response.json(); * return json({ customerInvite }); * }; * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; * * const shopify = shopifyApp({ * restResources, * // ...etc * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` */ rest: RestClientWithResources; /** * 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 * Querying the GraphQL API. * Use `admin.graphql` to make query / mutation requests. * ```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 * Handling GraphQL errors. * Catch `GraphqlQueryError` errors to see error messages from the API. * ```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; } ```` ### RestClientWithResources ```ts RemixRestClient & {resources: Resources} ``` ### RemixRestClient * session ```ts Session ``` * get Performs a GET request on the given path. ```ts (params: GetRequestParams) => Promise ``` * post Performs a POST request on the given path. ```ts (params: PostRequestParams) => Promise ``` * put Performs a PUT request on the given path. ```ts (params: PostRequestParams) => Promise ``` * delete Performs a DELETE request on the given path. ```ts (params: GetRequestParams) => Promise ``` ```ts class RemixRestClient { public session: Session; private params: AdminClientOptions['params']; private handleClientError: AdminClientOptions['handleClientError']; constructor({params, session, handleClientError}: AdminClientOptions) { this.params = params; this.handleClientError = handleClientError; this.session = session; } /** * Performs a GET request on the given path. */ public async get(params: GetRequestParams) { return this.makeRequest({ method: 'GET' as RequestParams['method'], ...params, }); } /** * Performs a POST request on the given path. */ public async post(params: PostRequestParams) { return this.makeRequest({ method: 'POST' as RequestParams['method'], ...params, }); } /** * Performs a PUT request on the given path. */ public async put(params: PutRequestParams) { return this.makeRequest({ method: 'PUT' as RequestParams['method'], ...params, }); } /** * Performs a DELETE request on the given path. */ public async delete(params: DeleteRequestParams) { return this.makeRequest({ method: 'DELETE' as RequestParams['method'], ...params, }); } protected async makeRequest(params: RequestParams): Promise { const originalClient = new this.params.api.clients.Rest({ session: this.session, }); const originalRequest = Reflect.get(originalClient, 'request'); try { const apiResponse = await originalRequest.call(originalClient, params); // We use a separate client for REST requests and REST resources because we want to override the API library // client class to return a Response object instead. return new Response(JSON.stringify(apiResponse.body), { headers: apiResponse.headers, }); } catch (error) { if (this.handleClientError) { throw await this.handleClientError({ error, session: this.session, params: this.params, }); } else throw new Error(error); } } } ``` ### GetRequestParams * path The path to the resource, relative to the API version root. ```ts string ``` * type The type of data expected in the response. ```ts DataType ``` * data The request body. ```ts string | Record ``` * query Query parameters to be sent with the request. ```ts SearchParams ``` * extraHeaders Additional headers to be sent with the request. ```ts HeaderParams ``` * tries The maximum number of times the request can be made if it fails with a throttling or server error. ```ts number ``` ```ts export interface GetRequestParams { /** * The path to the resource, relative to the API version root. */ path: string; /** * The type of data expected in the response. */ type?: DataType; /** * The request body. */ data?: Record | string; /** * Query parameters to be sent with the request. */ query?: SearchParams; /** * Additional headers to be sent with the request. */ extraHeaders?: HeaderParams; /** * The maximum number of times the request can be made if it fails with a throttling or server error. */ tries?: number; } ``` ### DataType * JSON ```ts application/json ``` * GraphQL ```ts application/graphql ``` * URLEncoded ```ts application/x-www-form-urlencoded ``` ```ts export enum DataType { JSON = 'application/json', GraphQL = 'application/graphql', URLEncoded = 'application/x-www-form-urlencoded', } ``` ### HeaderParams Headers to be sent with the request. ```ts Record ``` ### PostRequestParams ```ts GetRequestParams & { data: Record | string; } ``` ### GraphQLClient * query ```ts Operation extends keyof Operations ``` * options ```ts GraphQLQueryOptions ``` interface Promise\ { /\*\* \* 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\(onfulfilled?: ((value: T) => TResult1 | PromiseLike\) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike\) | undefined | null): Promise\; /\*\* \* 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\(onrejected?: ((reason: any) => TResult | PromiseLike\) | undefined | null): Promise\; }, interface Promise\ {}, Promise: PromiseConstructor, interface Promise\ { readonly \[Symbol.toStringTag]: string; }, interface Promise\ { /\*\* \* 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\; } ```ts interface Promise { /** * 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(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise; /** * 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(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise; }, interface Promise {}, Promise: PromiseConstructor, interface Promise { readonly [Symbol.toStringTag]: string; }, interface Promise { /** * 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; } ``` ```ts export type GraphQLClient = < Operation extends keyof Operations, >( query: Operation, options?: GraphQLQueryOptions, ) => Promise>; ``` ### GraphQLQueryOptions * variables The variables to pass to the operation. ```ts ApiClientRequestOptions["variables"] ``` * apiVersion The version of the API to use for the request. ```ts ApiVersion ``` * headers Additional headers to include in the request. ```ts Record ``` * tries The total number of times to try the request if it fails. ```ts number ``` ```ts export interface GraphQLQueryOptions< Operation extends keyof Operations, Operations extends AllOperations, > { /** * The variables to pass to the operation. */ variables?: ApiClientRequestOptions['variables']; /** * The version of the API to use for the request. */ apiVersion?: ApiVersion; /** * Additional headers to include in the request. */ headers?: Record; /** * The total number of times to try the request if it fails. */ tries?: number; } ``` ### ApiVersion * October22 ```ts 2022-10 ``` * January23 ```ts 2023-01 ``` * April23 ```ts 2023-04 ``` * July23 ```ts 2023-07 ``` * October23 ```ts 2023-10 ``` * January24 ```ts 2024-01 ``` * April24 ```ts 2024-04 ``` * Unstable ```ts unstable ``` ```ts export enum ApiVersion { October22 = '2022-10', January23 = '2023-01', April23 = '2023-04', July23 = '2023-07', October23 = '2023-10', January24 = '2024-01', April24 = '2024-04', Unstable = 'unstable', } ``` ### BillingContext * require Checks if the shop has an active payment for any plan defined in the \`billing\` config option. ```ts (options: RequireBillingOptions) => Promise ``` * check Checks if the shop has an active payment for any plan defined in the \`billing\` config option. ```ts (options: CheckBillingOptions) => Promise ``` * request Requests payment for the plan. ```ts (options: RequestBillingOptions) => Promise ``` * cancel Cancels an ongoing subscription, given its ID. ```ts (options: CancelBillingOptions) => Promise ``` ````ts export interface BillingContext { /** * 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 * Requesting billing right away. * Call `billing.request` in the `onFailure` callback to immediately redirect to the Shopify page to request payment. * ```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'; * export const ANNUAL_PLAN = 'Annual subscription'; * * const shopify = shopifyApp({ * // ...etc * billing: { * [MONTHLY_PLAN]: { * amount: 5, * currencyCode: 'USD', * interval: BillingInterval.Every30Days, * }, * [ANNUAL_PLAN]: { * amount: 50, * currencyCode: 'USD', * interval: BillingInterval.Annual, * }, * } * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` * * @example * Redirect 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. * ```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]: { * amount: 5, * currencyCode: 'USD', * interval: BillingInterval.Every30Days, * }, * [ANNUAL_PLAN]: { * amount: 50, * currencyCode: 'USD', * interval: BillingInterval.Annual, * }, * } * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` * @example * Requesting billing with line items * Call `billing.request` with the `v3_lineItemBilling` future flag enabled * ```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'; * export const ANNUAL_PLAN = 'Annual subscription'; * * const shopify = shopifyApp({ * // ...etc * billing: { * [MONTHLY_PLAN]: { * lineItems: [ * { * amount: 5, * currencyCode: 'USD', * interval: BillingInterval.Every30Days, * }, * { * amount: 1, * currencyCode: 'USD', * interval: BillingInterval.Usage. * terms: '1 dollar per 1000 emails', * }, * ], * }, * } * future: {v3_lineItemBilling: true} * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` */ require: ( options: RequireBillingOptions, ) => Promise; /** * 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 * Check 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 not * throw an error if no active billing plans are present. * ```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]: { * amount: 5, * currencyCode: 'USD', * interval: BillingInterval.Every30Days, * }, * [ANNUAL_PLAN]: { * amount: 50, * currencyCode: 'USD', * interval: BillingInterval.Annual, * }, * } * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` * */ check: ( options: CheckBillingOptions, ) => Promise; /** * Requests payment for the plan. * * @returns Redirects to the confirmation URL for the payment. * * @example * Using a custom return URL. * Change where the merchant is returned to after approving the purchase using the `returnUrl` option. * ```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]: { * amount: 5, * currencyCode: 'USD', * interval: BillingInterval.Every30Days, * }, * [ANNUAL_PLAN]: { * amount: 50, * currencyCode: 'USD', * interval: BillingInterval.Annual, * }, * } * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` */ request: (options: RequestBillingOptions) => Promise; /** * Cancels an ongoing subscription, given its ID. * * @returns The cancelled subscription. * * @example * Cancelling a subscription. * Use the `billing.cancel` function to cancel an active subscription with the id returned from `billing.require`. * ```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]: { * amount: 5, * currencyCode: 'USD', * interval: BillingInterval.Every30Days, * }, * [ANNUAL_PLAN]: { * amount: 50, * currencyCode: 'USD', * interval: BillingInterval.Annual, * }, * } * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` */ cancel: (options: CancelBillingOptions) => Promise; } ```` ### RequireBillingOptions * plans The plans to check for. Must be one of the values defined in the \`billing\` config option. ```ts (keyof Config["billing"])[] ``` * onFailure How to handle the request if the shop doesn't have an active payment for any plan. ```ts (error: any) => Promise ``` * isTest Whether to consider test purchases. ```ts boolean ``` ```ts export interface RequireBillingOptions extends Omit { /** * 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; } ``` ### BillingCheckResponseObject * hasActivePayment Whether the user has an active payment method. ```ts boolean ``` * oneTimePurchases The one-time purchases the shop has. ```ts OneTimePurchase[] ``` * appSubscriptions The active subscriptions the shop has. ```ts AppSubscription[] ``` ```ts export interface BillingCheckResponseObject { /** * Whether the user has an active payment method. */ hasActivePayment: boolean; /** * The one-time purchases the shop has. */ oneTimePurchases: OneTimePurchase[]; /** * The active subscriptions the shop has. */ appSubscriptions: AppSubscription[]; } ``` ### OneTimePurchase * id The ID of the one-time purchase. ```ts string ``` * name The name of the purchased plan. ```ts string ``` * test Whether this is a test purchase. ```ts boolean ``` * status The status of the one-time purchase. ```ts string ``` ```ts export interface OneTimePurchase { /** * The ID of the one-time purchase. */ id: string; /** * The name of the purchased plan. */ name: string; /** * Whether this is a test purchase. */ test: boolean; /** * The status of the one-time purchase. */ status: string; } ``` ### AppSubscription * id The ID of the app subscription. ```ts string ``` * name The name of the purchased plan. ```ts string ``` * test Whether this is a test subscription. ```ts boolean ``` * lineItems ```ts ActiveSubscriptionLineItem[] ``` ```ts export interface AppSubscription { /** * The ID of the app subscription. */ id: string; /** * The name of the purchased plan. */ name: string; /** * Whether this is a test subscription. */ test: boolean; /* * The line items for this plan. This will become mandatory in v10. */ lineItems?: ActiveSubscriptionLineItem[]; } ``` ### ActiveSubscriptionLineItem * id ```ts string ``` * plan ```ts AppPlan ``` ```ts export interface ActiveSubscriptionLineItem { /* * The ID of the line item. */ id: string; /* * The details of the plan. */ plan: AppPlan; } ``` ### AppPlan * pricingDetails ```ts RecurringAppPlan | UsageAppPlan ``` ```ts export interface AppPlan { /* * The pricing details of the plan. */ pricingDetails: RecurringAppPlan | UsageAppPlan; } ``` ### RecurringAppPlan * interval ```ts BillingInterval.Every30Days | BillingInterval.Annual ``` * price ```ts Money ``` * discount ```ts AppPlanDiscount ``` ```ts export interface RecurringAppPlan { /* * The interval for this plan is charged on. */ interval: BillingInterval.Every30Days | BillingInterval.Annual; /* * The price of the plan. */ price: Money; /* * The discount applied to the plan. */ discount: AppPlanDiscount; } ``` ### BillingInterval * OneTime ```ts ONE_TIME ``` * Every30Days ```ts EVERY_30_DAYS ``` * Annual ```ts ANNUAL ``` * Usage ```ts USAGE ``` ```ts export enum BillingInterval { OneTime = 'ONE_TIME', Every30Days = 'EVERY_30_DAYS', Annual = 'ANNUAL', Usage = 'USAGE', } ``` ### Money * amount ```ts number ``` * currencyCode ```ts string ``` ```ts interface Money { amount: number; currencyCode: string; } ``` ### AppPlanDiscount * durationLimitInIntervals ```ts number ``` * remainingDurationInIntervals ```ts number ``` * priceAfterDiscount ```ts Money ``` * value ```ts AppPlanDiscountAmount ``` ```ts export interface AppPlanDiscount { /* * The total number of intervals the discount applies to. */ durationLimitInIntervals: number; /* * The remaining number of intervals the discount applies to. */ remainingDurationInIntervals: number; /* * The price after the discount is applied. */ priceAfterDiscount: Money; /* * The value of the discount applied every billing interval. */ value: AppPlanDiscountAmount; } ``` ### AppPlanDiscountAmount ```ts BillingConfigSubscriptionPlanDiscountAmount | BillingConfigSubscriptionPlanDiscountPercentage ``` ### BillingConfigSubscriptionPlanDiscountAmount * amount The amount to discount. Cannot be set if \`percentage\` is set. ```ts number ``` * percentage The percentage to discount. Cannot be set if \`amount\` is set. ```ts never ``` ```ts export interface BillingConfigSubscriptionPlanDiscountAmount { /** * The amount to discount. * * Cannot be set if `percentage` is set. */ amount: number; /** * The percentage to discount. * * Cannot be set if `amount` is set. */ percentage?: never; } ``` ### BillingConfigSubscriptionPlanDiscountPercentage * amount The amount to discount. Cannot be set if \`percentage\` is set. ```ts never ``` * percentage The percentage to discount. Cannot be set if \`amount\` is set. ```ts number ``` ```ts export interface BillingConfigSubscriptionPlanDiscountPercentage { /** * The amount to discount. * * Cannot be set if `percentage` is set. */ amount?: never; /** * The percentage to discount. * * Cannot be set if `amount` is set. */ percentage: number; } ``` ### UsageAppPlan * balanceUsed ```ts Money ``` * cappedAmount ```ts Money ``` * terms ```ts string ``` ```ts export interface UsageAppPlan { /* * The total usage records for interval. */ balanceUsed: Money; /* * The capped amount prevents the merchant from being charged for any usage over that amount during a billing period. */ cappedAmount: Money; /* * The terms and conditions for app usage pricing. */ terms: string; } ``` ### CheckBillingOptions * plans The plans to check for. Must be one of the values defined in the \`billing\` config option. ```ts (keyof Config["billing"])[] ``` * isTest Whether to consider test purchases. ```ts boolean ``` ```ts export interface CheckBillingOptions extends Omit { /** * The plans to check for. Must be one of the values defined in the `billing` config option. */ plans: (keyof Config['billing'])[]; } ``` ### RequestBillingOptions * plan The plan to request. Must be one of the values defined in the \`billing\` config option. ```ts keyof Config["billing"] ``` * isTest Whether to use the test mode. This prevents the credit card from being charged. Test shops and demo shops cannot be charged. ```ts boolean ``` * returnUrl The URL to return to after the merchant approves the payment. ```ts string ``` ```ts export interface RequestBillingOptions extends Omit { /** * 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; } ``` ### CancelBillingOptions * subscriptionId The ID of the subscription to cancel. ```ts string ``` * prorate Whether to prorate the cancellation. ```ts boolean ``` * isTest ```ts boolean ``` ```ts export interface CancelBillingOptions { /** * The ID of the subscription to cancel. */ subscriptionId: string; /** * Whether to prorate the cancellation. * * {@link https://shopify.dev/docs/apps/billing/subscriptions/cancel-recurring-charges} */ prorate?: boolean; /* * Whether to use the test mode. This prevents the credit card from being charged. Test shops and demo shops cannot be charged. */ isTest?: boolean; } ``` ### EnsureCORSFunction ```ts export interface EnsureCORSFunction { (response: Response): Response; } ``` ### EmbeddedAdminContext * sessionToken The decoded and validated session token for the request. Returned only if \`isEmbeddedApp\` is \`true\`. ```ts JwtPayload ``` * 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\`. ```ts 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. ```ts Session ``` * admin Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request. ```ts AdminApiContext ``` * billing Billing methods for this store, based on the plans defined in the \`billing\` config option. ```ts BillingContext ``` * cors A function that ensures the CORS headers are set correctly for the response. ```ts EnsureCORSFunction ``` ````ts export interface EmbeddedAdminContext< Config extends AppConfigArg, Resources extends ShopifyRestResources = ShopifyRestResources, > extends AdminContextInternal { /** * 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 * Using the decoded session token. * Get user-specific data using the `sessionToken` object. * ```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 * Redirecting to an app route. * Use the `redirect` helper to safely redirect between pages. * ```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 * Redirecting outside of Shopify admin. * Pass in a `target` option of `_top` or `_parent` to go to an external URL. * ```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; } ```` ### JwtPayload * iss The shop's admin domain. ```ts string ``` * dest The shop's domain. ```ts string ``` * aud The client ID of the receiving app. ```ts string ``` * sub The User that the session token is intended for. ```ts string ``` * exp When the session token expires. ```ts number ``` * nbf When the session token activates. ```ts number ``` * iat When the session token was issued. ```ts number ``` * jti A secure random UUID. ```ts string ``` * sid A unique session ID per user and app. ```ts string ``` ```ts export interface JwtPayload { /** * The shop's admin domain. */ iss: string; /** * The shop's domain. */ dest: string; /** * The client ID of the receiving app. */ aud: string; /** * The User that the session token is intended for. */ sub: string; /** * When the session token expires. */ exp: number; /** * When the session token activates. */ nbf: number; /** * When the session token was issued. */ iat: number; /** * A secure random UUID. */ jti: string; /** * A unique session ID per user and app. */ sid: string; } ``` ### RedirectFunction * url ```ts string ``` * init ```ts RedirectInit ``` TypedResponse\ ```ts TypedResponse ``` ```ts export type RedirectFunction = ( url: string, init?: RedirectInit, ) => TypedResponse; ``` ### RedirectInit ```ts number | (ResponseInit & {target?: RedirectTarget}) ``` ### RedirectTarget ```ts '_self' | '_parent' | '_top' ``` Examples ### Examples * #### Authenticate, run API mutation, and redirect ##### Description Authenticate, run API mutation, and redirect ##### /app/routes/\*\*.ts ```typescript 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}); } }; ``` * #### Using the decoded session token ##### Description Get user-specific data using the \`sessionToken\` object. ##### /app/routes/\*\*\\/\*.ts ```typescript 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 ```typescript import { shopifyApp } from "@shopify/shopify-app-remix/server"; const shopify = shopifyApp({ // ...etc useOnlineTokens: true, }); export default shopify; export const authenticate = shopify.authenticate; ``` * #### Redirecting to an app route ##### Description Use the \`redirect\` helper to safely redirect between pages. ##### /app/routes/admin/my-route.ts ```typescript 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 outside of Shopify admin ##### Description Pass in a \`target\` option of \`\_top\` or \`\_parent\` to go to an external URL. ##### /app/routes/admin/my-route.ts ```typescript 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' }); }; ``` * #### Using offline sessions ##### Description Get your app's shop-specific data using an offline session. ##### /app/routes/\*\*\\/\*.ts ```typescript 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 ```typescript 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 ```typescript 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 ```typescript import { shopifyApp } from "@shopify/shopify-app-remix/server"; const shopify = shopifyApp({ // ...etc useOnlineTokens: true, }); export default shopify; export const authenticate = shopify.authenticate; ``` * #### 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 ```typescript 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}))); }; ``` * #### Using REST resources ##### Description Getting the number of orders in a store using REST resources. Visit the \[Admin REST API references]\(/docs/api/admin-rest) for examples on using each resource. ##### /app/routes/\*\*\\/\*.ts ```typescript import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { admin, session, } = await authenticate.admin(request); return json( admin.rest.resources.Order.count({ session }), ); }; ``` ##### /app/shopify.server.ts ```typescript import { shopifyApp } from "@shopify/shopify-app-remix/server"; import { restResources } from "@shopify/shopify-api/rest/admin/2023-07"; const shopify = shopifyApp({ restResources, // ...etc }); export default shopify; export const authenticate = shopify.authenticate; ``` * #### Performing a GET request to the REST API ##### Description Use \`admin.rest.get\` to make custom requests to make a request to to the \`customer/count\` endpoint ##### /app/routes/\*\*\\/\*.ts ```typescript import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { admin, session, } = await authenticate.admin(request); const response = await admin.rest.get({ path: "/customers/count.json", }); const customers = await response.json(); return json({ customers }); }; ``` ##### /app/shopify.server.ts ```typescript import { shopifyApp } from "@shopify/shopify-app-remix/server"; import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; const shopify = shopifyApp({ restResources, // ...etc }); export default shopify; export const authenticate = shopify.authenticate; ``` * #### Performing a POST request to the REST API ##### Description Use \`admin.rest.post\` to make custom requests to make a request to to the \`customers.json\` endpoint to send a welcome email ##### /app/routes/\*\*\\/\*.ts ```typescript import { LoaderFunctionArgs, json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export const loader = async ({ request }: LoaderFunctionArgs) => { const { admin, session, } = await authenticate.admin(request); const response = admin.rest.post({ path: "customers/7392136888625/send_invite.json", body: { customer_invite: { to: "new_test_email@shopify.com", from: "j.limited@example.com", bcc: ["j.limited@example.com"], subject: "Welcome to my new shop", custom_message: "My awesome new store", }, }, }); const customerInvite = await response.json(); return json({ customerInvite }); }; ``` ##### /app/shopify.server.ts ```typescript import { shopifyApp } from "@shopify/shopify-app-remix/server"; import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; const shopify = shopifyApp({ restResources, // ...etc }); export default shopify; export const authenticate = shopify.authenticate; ``` * #### Querying the GraphQL API ##### Description Use \`admin.graphql\` to make query / mutation requests. ##### /app/routes/\*\*\\/\*.ts ```typescript 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 ```typescript 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 ```typescript 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 ```typescript import { shopifyApp } from "@shopify/shopify-app-remix/server"; const shopify = shopifyApp({ // ... }); export default shopify; export const authenticate = shopify.authenticate; ``` * #### 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 ```typescript 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 ```typescript 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]: { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, }, [ANNUAL_PLAN]: { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, }, } }); 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 ```typescript 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 ```typescript 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]: { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, }, [ANNUAL_PLAN]: { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, }, } }); export default shopify; export const authenticate = shopify.authenticate; ``` * #### Requesting billing with line items ##### Description Call \`billing.request\` with the \`v3\_lineItemBilling\` future flag enabled ##### /app/routes/\*\*\\/\*.ts ```typescript 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 ```typescript 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, }, { amount: 1, currencyCode: 'USD', interval: BillingInterval.Usage. terms: '1 dollar per 1000 emails', }, ], }, } future: {v3_lineItemBilling: true} }); export default shopify; export const authenticate = shopify.authenticate; ``` * #### 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 ```typescript 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 ```typescript 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]: { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, }, [ANNUAL_PLAN]: { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, }, } }); export default shopify; export const authenticate = shopify.authenticate; ``` * #### Using a custom return URL ##### Description Change where the merchant is returned to after approving the purchase using the \`returnUrl\` option. ##### /app/routes/\*\*\\/\*.ts ```typescript 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 ```typescript 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]: { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, }, [ANNUAL_PLAN]: { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, }, } }); export default shopify; export const authenticate = shopify.authenticate; ``` * #### 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 ```typescript 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 ```typescript 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]: { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, }, [ANNUAL_PLAN]: { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, }, } }); export default shopify; export const authenticate = shopify.authenticate; ``` ## Related [Interact with the Admin API. - API context](https://shopify.dev/docs/api/shopify-app-remix/apis/admin-api) [Bill merchants for your app using the Admin API. - Billing context](https://shopify.dev/docs/api/shopify-app-remix/apis/billing)