Connect an application with OAuth
Register your application, obtain consent, and call store APIs with scoped access tokens.
OAuth lets a user connect selected SellApp stores to your application without sharing an API key. Your application can then call supported v2 business endpoints within the approved scopes and the user's current permissions. These requests operate on real store data. Request only the access your application needs.
Register your application
Open OAuth applications from the dashboard's Developer settings. Enter an application name, choose its type, and enter one redirect URI per line.
| Application type | Redirects | Secret |
|---|---|---|
| Public native client | HTTP loopback IP templates | No client secret |
| Confidential web client | Exact HTTPS redirect URIs | Keep the secret on your server |
For a native application, register http://127.0.0.1/callback or
http://[::1]/callback. RFC 8252 loopback matching permits an ephemeral port,
such as http://127.0.0.1:49152/callback, when you start authorization. The scheme,
loopback IP, and path must match the registered template. Use the same actual
redirect URI, including its port, when exchanging the code.
localhost, wildcard hosts, non-loopback HTTP redirects, and fragments are
rejected. Confidential web clients use exact HTTPS redirects; the native port
exception does not apply to them.
Copy the client ID. A confidential client secret is displayed only after creation or rotation. Store it securely before leaving the page. Native clients have no secret and must not embed one in their application.
The Manage page lets you edit the name and redirects; the application type cannot change. It displays the active installation count and provides Revoke all installations, Rotate client secret for confidential clients, and Delete application. Rotation invalidates the old secret and revokes all existing installations and token families. Deletion revokes the application and all its installations. Users must authorize again after revocation.
Prepare PKCE and state
Proof Key for Code Exchange (PKCE) binds the authorization code to the application
that started the request. Both client types require PKCE S256 and a non-empty
state. Generate fresh values for each authorization attempt. Keep the verifier
and state with that attempt until the callback is validated.
The following Bash examples need Node.js, cURL, and jq. Supply your registered client ID through your application's configuration. Disable shell tracing and HTTP debug logging before handling credentials.
set -euo pipefail
export SELLAPP_ORIGIN='https://sell.app'
export SELLAPP_API_BASE_URL="${SELLAPP_ORIGIN}/api"
export SELLAPP_REDIRECT_URI='http://127.0.0.1:49152/callback'
export SELLAPP_SCOPES='stores:read products:read orders:read'
: "${SELLAPP_CLIENT_ID:?Set your registered client ID}"
export SELLAPP_CODE_VERIFIER="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("base64url"))')"
export SELLAPP_STATE="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("base64url"))')"
export SELLAPP_CODE_CHALLENGE="$(node -e 'process.stdout.write(require("node:crypto").createHash("sha256").update(process.env.SELLAPP_CODE_VERIFIER).digest("base64url"))')"
node <<'JS'
const url = new URL('/oauth/authorize', process.env.SELLAPP_ORIGIN);
url.search = new URLSearchParams({
response_type: 'code',
client_id: process.env.SELLAPP_CLIENT_ID,
redirect_uri: process.env.SELLAPP_REDIRECT_URI,
scope: process.env.SELLAPP_SCOPES,
state: process.env.SELLAPP_STATE,
code_challenge: process.env.SELLAPP_CODE_CHALLENGE,
code_challenge_method: 'S256',
}).toString();
console.log(url.toString());
JSStart your native callback listener on the chosen loopback address and port
before opening this URL in the user's browser. For a confidential web application,
set SELLAPP_REDIRECT_URI to its registered HTTPS callback instead.
Obtain consent and validate the callback
The browser takes the user to SellApp to log in, review the requested scopes, and select the stores the application may access. SellApp's consent form handles approval or cancellation; do not collect the user's password in your application.
At your callback, compare the returned state with the saved value using a
constant-time comparison. Reject missing, mismatched, or already-used state
before exchanging a code. If the callback contains an OAuth error, stop that
attempt and explain it to the user. On success, save the returned code as
SELLAPP_AUTHORIZATION_CODE in memory and consume the saved state. Do not put
codes in logs or analytics.
Exchange the authorization code
Send the code immediately with the verifier and the exact redirect URI used in authorization. A code is single-use. This example keeps the token response in memory instead of printing it:
SELLAPP_TOKEN_RESPONSE="$(curl --fail-with-body --silent --show-error \
--request POST "${SELLAPP_ORIGIN}/oauth/token" \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode "client_id=${SELLAPP_CLIENT_ID}" \
--data-urlencode "code=${SELLAPP_AUTHORIZATION_CODE}" \
--data-urlencode "redirect_uri=${SELLAPP_REDIRECT_URI}" \
--data-urlencode "code_verifier=${SELLAPP_CODE_VERIFIER}")"
export SELLAPP_ACCESS_TOKEN="$(printf '%s' "$SELLAPP_TOKEN_RESPONSE" | jq -er '.access_token')"
export SELLAPP_REFRESH_TOKEN="$(printf '%s' "$SELLAPP_TOKEN_RESPONSE" | jq -er '.refresh_token')"
unset SELLAPP_TOKEN_RESPONSE SELLAPP_AUTHORIZATION_CODE SELLAPP_CODE_VERIFIERThe success response contains token_type, expires_in, access_token, and
refresh_token. Use expires_in to schedule refresh. For a confidential client,
also add --data-urlencode "client_secret=${SELLAPP_CLIENT_SECRET}" to the token
request. Keep that request on the server. Client HTTP Basic authentication is
also supported; use one client-authentication method per request.
Select a store and call its API
List the stores the installation can access, then use a returned slug in
X-STORE. This list is also limited by the user's current store membership.
curl --fail-with-body --silent --show-error \
"${SELLAPP_API_BASE_URL}/v2/stores" \
--header "Authorization: Bearer ${SELLAPP_ACCESS_TOKEN}" \
--header 'Accept: application/json'For example, a returned store can have id: "42", slug: "launch-lab", and
name: "Launch Lab". Use its numeric identifier with /v2/stores/{store} and
its slug in the header:
export SELLAPP_STORE_SLUG='launch-lab'
curl --fail-with-body --silent --show-error \
"${SELLAPP_API_BASE_URL}/v2/products?limit=1" \
--header "Authorization: Bearer ${SELLAPP_ACCESS_TOKEN}" \
--header "X-STORE: ${SELLAPP_STORE_SLUG}" \
--header 'Accept: application/json'This read requires products:read. OAuth store business requests require
X-STORE, even if the installation has only one store. Missing it returns
400. A token alone does not grant access to an unapproved store or restore
permissions the user has lost.
Check effective permissions
curl --fail-with-body --silent --show-error \
"${SELLAPP_API_BASE_URL}/v2/permissions" \
--header "Authorization: Bearer ${SELLAPP_ACCESS_TOKEN}" \
--header "X-STORE: ${SELLAPP_STORE_SLUG}" \
--header 'Accept: application/json'The data object contains four distinct arrays:
| Field | Meaning |
|---|---|
store_permissions | The user's current permissions in this store. |
approved_scopes | Scopes approved for the active installation. |
token_scopes | Scopes carried by this access token. |
effective_scopes | Currently usable scopes after installation approval, token grants, selected-store access, and current role permissions are combined. |
Approval is not a permanent permission grant. Check the operation's requirements
and handle permission changes. 401 means authentication failed; 403 means
access is denied; 404 can mean the requested store or resource is unavailable
to this caller. Do not retry these errors unchanged.
Available scopes
Read scopes cover reads and POST search operations. Write scopes cover mutations; a write scope does not automatically include the corresponding read scope.
| Scope | Access |
|---|---|
profile:read | Read the authenticated profile. |
stores:read | Read accessible stores and effective permissions. |
orders:read | Read orders, refunds, bookings, and subscriptions. |
orders:write | Create and manage orders, refunds, bookings, and subscriptions. |
products:read | Read products and catalog configuration. |
products:write | Manage products and catalog configuration. |
customers:read | Read customers and feedback. |
customers:write | Manage customers and feedback. |
support:read | Read tickets and blacklists. |
support:write | Manage tickets and blacklists. |
webhooks:read | Read webhook configuration and deliveries. |
webhooks:write | Manage webhook configuration and deliveries. |
payments:read | Read charges, customer credits, affiliate programs, and affiliate payout records. |
payments:write | Manage charges, customer credits, affiliate programs, and affiliate payout records. |
community:read | Read community configuration. |
community:write | Manage community configuration. |
store:read | Read store settings. |
store:write | Manage store settings. |
These are the scope catalogue labels. The endpoint reference gives the exact
requirement: current customer and entitlement routes use orders:read or
orders:write, while feedback uses customers:read or customers:write.
Order, invoice, refund, subscription, booking, and license operations use
orders:*; catalog operations use products:*. Payment methods, customer
credits, rewards, wallets, and merchant-managed affiliate payout records use
payments:*. Affiliate payout records do not mean SellApp transfers funds.
Exports select authorization by report category.
Rotate refresh tokens safely
Refresh returns a new access token and refresh token. Use one refresh operation at a time per installation and atomically replace both stored tokens: either save the complete new pair or retain neither as a usable new pair. Do not let concurrent workers reuse the old refresh token.
SELLAPP_TOKEN_RESPONSE="$(curl --fail-with-body --silent --show-error \
--request POST "${SELLAPP_ORIGIN}/oauth/token" \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode "client_id=${SELLAPP_CLIENT_ID}" \
--data-urlencode "refresh_token=${SELLAPP_REFRESH_TOKEN}")"A confidential client must also authenticate as in the code exchange. Validate the success response, save the replacement tokens together in secure storage, and discard the old pair. The shell variable above illustrates the HTTP exchange; your application must implement atomic storage.
Reusing a consumed refresh token revokes its token family. Treat reuse as a
possible compromise and investigate concurrent refreshes or credential exposure.
Do not blindly retry a refresh after losing its response: the token may already
have been consumed. After invalid_grant, stop retrying that credential and
start a fresh authorization flow with user consent.
Revoke access
To disconnect the installation associated with a token, send either an access token or refresh token to
the revocation endpoint. This revokes the associated installation and invalidates
its access and refresh tokens.
The optional token_type_hint accepts access_token or refresh_token.
curl --fail-with-body --silent --show-error \
--request POST "${SELLAPP_ORIGIN}/oauth/revoke" \
--header 'Accept: application/json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "client_id=${SELLAPP_CLIENT_ID}" \
--data-urlencode "token=${SELLAPP_REFRESH_TOKEN}" \
--data-urlencode 'token_type_hint=refresh_token'For confidential clients, add the client secret to this body or use HTTP Basic
authentication instead of both body credential fields. Do not combine Basic
authentication with body client_id or client_secret. Success returns 200
with [], including for an unknown token; it does not disclose whether a token
existed. Invalid client authentication returns 401 with error: "invalid_client".
To inspect the current installation, call GET /v2/oauth/installation with its
access token. You can also disconnect the current installation directly:
curl --fail-with-body --silent --show-error --request DELETE \
"${SELLAPP_API_BASE_URL}/v2/oauth/installation" \
--header "Authorization: Bearer ${SELLAPP_ACCESS_TOKEN}" \
--header 'Accept: application/json'This returns 204 and invalidates the installation's tokens. These installation
operations do not need X-STORE. Choose the appropriate disconnect operation;
an already-revoked access token cannot be used for the installation request.
Protect credentials
Never log authorization codes, access tokens, refresh tokens, client secrets,
PKCE verifiers, or authorization headers. Protect callback query strings from
access logs and analytics. Keep tokens in secure storage and treat access tokens
as opaque credentials; do not depend on a JWT iss claim.
OAuth authorization and token endpoints are ${SELLAPP_ORIGIN}/oauth/authorize
and ${SELLAPP_ORIGIN}/oauth/token; refresh uses the same token endpoint.
Revocation uses ${SELLAPP_ORIGIN}/oauth/revoke. Discover the canonical endpoints
at authorization-server metadata.
See the OAuth reference for full request and response contracts.