Adminobject
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.
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
AuthenticateAdmin
- request
Request
Promise<AdminContext<Config, Resources>>
export type AuthenticateAdmin<
Config extends AppConfigArg,
Resources extends ShopifyRestResources = ShopifyRestResources,
> = (request: Request) => Promise<AdminContext<Config, Resources>>;
AdminContext
Config['isEmbeddedApp'] extends false
? NonEmbeddedAdminContext<Config, Resources>
: EmbeddedAdminContext<Config, Resources>
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.
Session
- admin
Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.
AdminApiContext<Resources>
- 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
export interface NonEmbeddedAdminContext<
Config extends AppConfigArg,
Resources extends ShopifyRestResources = ShopifyRestResources,
> extends AdminContextInternal<Config, Resources> {}
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.
string
- shop
The Shopify shop domain, such as `example.myshopify.com`.
string
- state
The state of the session. Used for the OAuth authentication code flow.
string
- isOnline
Whether the access token in the session is online or offline.
boolean
- scope
The desired scopes for the access token, at the time the session was created.
string
- expires
The date the access token expires.
Date
- accessToken
The access token for the session.
string
- onlineAccessInfo
Information on the user for the session. Only present for online sessions.
OnlineAccessInfo
- isActive
Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes.
(scopes: string | string[] | AuthScopes) => boolean
- isScopeChanged
Whether the access token has the given scopes.
(scopes: string | string[] | AuthScopes) => boolean
- isExpired
Whether the access token is expired.
(withinMillisecondsOfExpiry?: number) => boolean
- toObject
Converts an object with data into a Session.
() => SessionParams
- equals
Checks whether the given session is equal to this session.
(other: Session) => boolean
- toPropertyArray
Converts the session into an array of key-value pairs.
(returnUserData?: boolean) => [string, string | number | boolean][]
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.
number
- associated_user_scope
The effective set of scopes for the session.
string
- associated_user
The user associated with the access token.
OnlineAccessUser
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.
number
- first_name
The user's first name.
string
- last_name
The user's last name.
string
- email
The user's email address.
string
- email_verified
Whether the user has verified their email address.
boolean
- account_owner
Whether the user is the account owner.
boolean
- locale
The user's locale.
string
- collaborator
Whether the user is a collaborator.
boolean
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.
(scope: string | string[] | AuthScopes) => boolean
- equals
Checks whether the current set of scopes equals the given one.
(otherScopes: string | string[] | AuthScopes) => boolean
- toString
Returns a comma-separated string with the current set of scopes.
() => string
- toArray
Returns an array with the current set of scopes.
() => any[]
class AuthScopes {
public static SCOPE_DELIMITER = ',';
private compressedScopes: Set<string>;
private expandedScopes: Set<string>;
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]
any
- id
The unique identifier for the session.
string
- shop
The Shopify shop domain.
string
- state
The state of the session. Used for the OAuth authentication code flow.
string
- isOnline
Whether the access token in the session is online or offline.
boolean
- scope
The scopes for the access token.
string
- expires
The date the access token expires.
Date
- accessToken
The access token for the session.
string
- onlineAccessInfo
Information on the user for the session. Only present for online sessions.
OnlineAccessInfo | StoredOnlineAccessInfo
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
Omit<OnlineAccessInfo, 'associated_user'> & {
associated_user: Partial<OnlineAccessUser>;
}
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.
RestClientWithResources<Resources>
- graphql
Methods for interacting with the Shopify Admin GraphQL API
GraphQLClient<AdminOperations>
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
* <caption>Using REST resources.</caption>
* <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. </description>
*
* ```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
* <caption>Performing a GET request to the REST API.</caption>
* <description>Use `admin.rest.get` to make custom requests to make a request to to the `customer/count` endpoint</description>
*
* ```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
* <caption>Performing a POST request to the REST API.</caption>
* <description>Use `admin.rest.post` to make custom requests to make a request to to the `customers.json` endpoint to send a welcome email</description>
* ```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<Resources>;
/**
* 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>;
}
RestClientWithResources
RemixRestClient & {resources: Resources}
RemixRestClient
- session
Session
- get
Performs a GET request on the given path.
(params: GetRequestParams) => Promise<Response>
- post
Performs a POST request on the given path.
(params: PostRequestParams) => Promise<Response>
- put
Performs a PUT request on the given path.
(params: PostRequestParams) => Promise<Response>
- delete
Performs a DELETE request on the given path.
(params: GetRequestParams) => Promise<Response>
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<Response> {
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.
string
- type
The type of data expected in the response.
DataType
- data
The request body.
string | Record<string, any>
- query
Query parameters to be sent with the request.
SearchParams
- extraHeaders
Additional headers to be sent with the request.
HeaderParams
- tries
The maximum number of times the request can be made if it fails with a throttling or server error.
number
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, any> | 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
application/json
- GraphQL
application/graphql
- URLEncoded
application/x-www-form-urlencoded
export enum DataType {
JSON = 'application/json',
GraphQL = 'application/graphql',
URLEncoded = 'application/x-www-form-urlencoded',
}
HeaderParams
Headers to be sent with the request.
Record<string, string | number | string[]>
PostRequestParams
GetRequestParams & {
data: Record<string, any> | string;
}
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>;
}
export type GraphQLClient<Operations extends AllOperations> = <
Operation extends keyof Operations,
>(
query: Operation,
options?: GraphQLQueryOptions<Operation, Operations>,
) => Promise<GraphQLResponse<Operation, Operations>>;
GraphQLQueryOptions
- variables
The variables to pass to the operation.
ApiClientRequestOptions<Operation, Operations>["variables"]
- apiVersion
The version of the API to use for the request.
ApiVersion
- headers
Additional headers to include in the request.
Record<string, any>
- tries
The total number of times to try the request if it fails.
number
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;
}
ApiVersion
- October22
2022-10
- January23
2023-01
- April23
2023-04
- July23
2023-07
- October23
2023-10
- January24
2024-01
- April24
2024-04
- Unstable
unstable
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.
(options: RequireBillingOptions<Config>) => Promise<BillingCheckResponseObject>
- check
Checks if the shop has an active payment for any plan defined in the `billing` config option.
(options: CheckBillingOptions<Config>) => Promise<BillingCheckResponseObject>
- request
Requests payment for the plan.
(options: RequestBillingOptions<Config>) => Promise<never>
- cancel
Cancels an ongoing subscription, given its ID.
(options: CancelBillingOptions) => Promise<AppSubscription>
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';
* 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
* <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]: {
* 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
* <caption>Requesting billing with line items</caption>
* <description>Call `billing.request` with the `v3_lineItemBilling` future flag enabled</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';
* 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<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]: {
* 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<Config>,
) => 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]: {
* 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<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]: {
* 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<AppSubscription>;
}
RequireBillingOptions
- plans
The plans to check for. Must be one of the values defined in the `billing` config option.
(keyof Config["billing"])[]
- onFailure
How to handle the request if the shop doesn't have an active payment for any plan.
(error: any) => Promise<Response>
- isTest
Whether to consider test purchases.
boolean
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>;
}
BillingCheckResponseObject
- hasActivePayment
Whether the user has an active payment method.
boolean
- oneTimePurchases
The one-time purchases the shop has.
OneTimePurchase[]
- appSubscriptions
The active subscriptions the shop has.
AppSubscription[]
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.
string
- name
The name of the purchased plan.
string
- test
Whether this is a test purchase.
boolean
- status
The status of the one-time purchase.
string
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.
string
- name
The name of the purchased plan.
string
- test
Whether this is a test subscription.
boolean
- lineItems
ActiveSubscriptionLineItem[]
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
string
- plan
AppPlan
export interface ActiveSubscriptionLineItem {
/*
* The ID of the line item.
*/
id: string;
/*
* The details of the plan.
*/
plan: AppPlan;
}
AppPlan
- pricingDetails
RecurringAppPlan | UsageAppPlan
export interface AppPlan {
/*
* The pricing details of the plan.
*/
pricingDetails: RecurringAppPlan | UsageAppPlan;
}
RecurringAppPlan
- interval
BillingInterval.Every30Days | BillingInterval.Annual
- price
Money
- discount
AppPlanDiscount
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
ONE_TIME
- Every30Days
EVERY_30_DAYS
- Annual
ANNUAL
- Usage
USAGE
export enum BillingInterval {
OneTime = 'ONE_TIME',
Every30Days = 'EVERY_30_DAYS',
Annual = 'ANNUAL',
Usage = 'USAGE',
}
Money
- amount
number
- currencyCode
string
interface Money {
amount: number;
currencyCode: string;
}
AppPlanDiscount
- durationLimitInIntervals
number
- remainingDurationInIntervals
number
- priceAfterDiscount
Money
- value
AppPlanDiscountAmount
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
BillingConfigSubscriptionPlanDiscountAmount | BillingConfigSubscriptionPlanDiscountPercentage
BillingConfigSubscriptionPlanDiscountAmount
- amount
The amount to discount. Cannot be set if `percentage` is set.
number
- percentage
The percentage to discount. Cannot be set if `amount` is set.
never
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.
never
- percentage
The percentage to discount. Cannot be set if `amount` is set.
number
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
Money
- cappedAmount
Money
- terms
string
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.
(keyof Config["billing"])[]
- isTest
Whether to consider test purchases.
boolean
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'])[];
}
RequestBillingOptions
- plan
The plan to request. Must be one of the values defined in the `billing` config option.
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.
boolean
- 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;
}
CancelBillingOptions
- subscriptionId
The ID of the subscription to cancel.
string
- prorate
Whether to prorate the cancellation.
boolean
- isTest
boolean
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
export interface EnsureCORSFunction {
(response: Response): Response;
}
EmbeddedAdminContext
- sessionToken
The decoded and validated session token for the request. Returned only if `isEmbeddedApp` is `true`.
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`.
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
- admin
Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request.
AdminApiContext<Resources>
- 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
export interface EmbeddedAdminContext<
Config extends AppConfigArg,
Resources extends ShopifyRestResources = ShopifyRestResources,
> extends AdminContextInternal<Config, Resources> {
/**
* 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 outside of Shopify admin.</caption>
* <description>Pass in a `target` option of `_top` or `_parent` to go to an external URL.</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;
}
JwtPayload
- iss
The shop's admin domain.
string
- dest
The shop's domain.
string
- aud
The client ID of the receiving app.
string
- sub
The User that the session token is intended for.
string
- exp
When the session token expires.
number
- nbf
When the session token activates.
number
- iat
When the session token was issued.
number
- jti
A secure random UUID.
string
- sid
A unique session ID per user and app.
string
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
string
- init
RedirectInit
TypedResponse<never>
export type RedirectFunction = (
url: string,
init?: RedirectInit,
) => TypedResponse<never>;
RedirectInit
number | (ResponseInit & {target?: RedirectTarget})
RedirectTarget
'_self' | '_parent' | '_top'
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-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-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-outside-of-shopify-adminRedirecting outside of Shopify admin
Pass in a target
option of _top
or _parent
to go to an external URL.
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 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
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-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-using-rest-resourcesUsing REST resources
Getting the number of orders in a store using REST resources. Visit the Admin REST API references for examples on using each resource.
Anchor to example-performing-a-get-request-to-the-rest-apiPerforming a GET request to the REST API
Use admin.rest.get
to make custom requests to make a request to to the endpoint
Anchor to example-performing-a-post-request-to-the-rest-apiPerforming 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
Using REST resources
examples
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
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
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
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
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
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
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;
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-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.
Anchor to example-requesting-billing-with-line-itemsRequesting billing with line items
Call billing.request
with the future flag enabled
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'; 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
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]: { 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
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'; 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;
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.
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]: { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, }, [ANNUAL_PLAN]: { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, }, } }); 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.
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]: { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, }, [ANNUAL_PLAN]: { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, }, } }); 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]: { amount: 5, currencyCode: 'USD', interval: BillingInterval.Every30Days, }, [ANNUAL_PLAN]: { amount: 50, currencyCode: 'USD', interval: BillingInterval.Annual, }, } }); export default shopify; export const authenticate = shopify.authenticate;