Store Hugs Developers

Apps

An app that many merchants connect gets an Admin key per shop, limited to the scopes the merchant approves, through OAuth with S256 PKCE.

  1. Register your app as an OAuth client once: POST https://my.storehugs.com/oauth/register (RFC 7591) with client_name and redirect_uris. Keep the client_id.
  2. Make a PKCE pair and a state, keep them in the merchant's session, and send the merchant to the page authorizeAppUrl builds.
  3. On your return address, check the state and trade the code for the shop's Admin key with exchangeAppCode, within 10 minutes.

createPkcePair() and createState() make the pair and the state (see PKCE).

The key is shown only that once. Approving your app again replaces the shop's earlier key. When the merchant revokes your app's key, the app is disconnected: the Storefront keys it made are revoked and its webhook endpoints deleted.

Build the approval page's address

AdminClient.authorizeAppUrl(params: {
    clientId: string;
    redirectUri: string;
    scopes: string[];
    state: string;
    codeChallenge: string;
    baseUrl?: string;
}): string

A static method that makes no request. It returns the page to send the merchant to, https://my.storehugs.com/integrations/authorize?…, where they confirm their password, pick a shop and approve your app's scopes.

Parameters

Parameter Type
params.clientId string Your app's client_id.
params.redirectUri string Your return address, exactly as registered.
params.scopes string[] The Admin scopes to ask for.
params.state string A random value to check on the way back.
params.codeChallenge string The PKCE pair's S256 challenge.
params.baseUrl string The panel's origin (default https://my.storehugs.com).

Returns

A string: the address, with these query values.

Query value
client_id Your app's client_id.
redirect_uri Your return address.
response_type Always code.
scope The scopes, separated by spaces.
state Your state.
code_challenge Your PKCE challenge.
code_challenge_method Always S256.

When the merchant approves, the browser returns to your address with a one-time code and your state. When they deny, it returns with error=access_denied.

Example

import {
    AdminClient,
    createPkcePair,
    createState,
} from '@storehugs/integration';

const { verifier, challenge } = await createPkcePair();
const state = createState();
// Keep verifier and state in the merchant's session.

const url = AdminClient.authorizeAppUrl({
    clientId: process.env.STOREHUGS_CLIENT_ID!,
    redirectUri: 'https://app.example.com/storehugs/callback',
    scopes: ['products:read', 'inventory:write', 'orders:read'],
    state,
    codeChallenge: challenge,
});

Exchange the code for an Admin key

AdminClient.exchangeAppCode(params: {
    code: string;
    clientId: string;
    redirectUri: string;
    codeVerifier: string;
    baseUrl?: string;
    fetch?: FetchLike;
}): Promise<AppKeyGrant>

A static method. Your server trades the one-time code and your PKCE verifier for the app's Admin key, at POST /api/admin/v1/oauth/token, with no key. The code lasts 10 minutes and is spent by its first try, so the call isn't retried and sends no Idempotency-Key.

Parameters

Parameter Type
params.code string The code your return address received.
params.clientId string Your app's client_id.
params.redirectUri string The same return address you sent the merchant with.
params.codeVerifier string The PKCE pair's verifier, kept from before the redirect.
params.baseUrl string The panel's origin (default https://my.storehugs.com).
params.fetch FetchLike Your own fetch.

Returns

An AppKeyGrant, shown only this once:

FieldTypeDescription
api_keystringThe Admin key (sa_live_…).
scopesarray of string
shopobject
shop.idinteger
shop.namestring

Errors

StatusWhen
400invalid_grant: the code is unknown, used, expired, or not this app's.
422Validation errors ({ message, errors }), or an idempotency refusal(idempotency_key_reused, idempotency_key_invalid).
429Rate limited.

Example

const grant = await AdminClient.exchangeAppCode({
    code,
    clientId: process.env.STOREHUGS_CLIENT_ID!,
    redirectUri: 'https://app.example.com/storehugs/callback',
    codeVerifier: verifier,
});

await saveShop(grant.shop.id, grant.api_key, grant.scopes);
const admin = new AdminClient({ apiKey: grant.api_key });