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.
- Register your app as an OAuth client once:
POST https://my.storehugs.com/oauth/register(RFC 7591) withclient_nameandredirect_uris. Keep theclient_id. - Make a PKCE pair and a
state, keep them in the merchant's session, and send the merchant to the pageauthorizeAppUrlbuilds. - On your return address, check the
stateand trade thecodefor the shop's Admin key withexchangeAppCode, 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:
| Field | Type | Description |
|---|---|---|
api_key | string | The Admin key (sa_live_…). |
scopes | array of string | |
shop | object | |
shop.id | integer | |
shop.name | string |
Errors
| Status | When |
|---|---|
400 | invalid_grant: the code is unknown, used, expired, or not this app's. |
422 | Validation errors ({ message, errors }), or an idempotency refusal(idempotency_key_reused, idempotency_key_invalid). |
429 | Rate 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 });