Guides

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 typeRedirectsSecret
Public native clientHTTP loopback IP templatesNo client secret
Confidential web clientExact HTTPS redirect URIsKeep 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.

Prepare a native authorization attempt
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());
JS

Start 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.

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:

Exchange a native client's code
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_VERIFIER

The 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.

List accessible stores
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:

Read products in the selected store
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

Check access for the selected store
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:

FieldMeaning
store_permissionsThe user's current permissions in this store.
approved_scopesScopes approved for the active installation.
token_scopesScopes carried by this access token.
effective_scopesCurrently 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.

ScopeAccess
profile:readRead the authenticated profile.
stores:readRead accessible stores and effective permissions.
orders:readRead orders, refunds, bookings, and subscriptions.
orders:writeCreate and manage orders, refunds, bookings, and subscriptions.
products:readRead products and catalog configuration.
products:writeManage products and catalog configuration.
customers:readRead customers and feedback.
customers:writeManage customers and feedback.
support:readRead tickets and blacklists.
support:writeManage tickets and blacklists.
webhooks:readRead webhook configuration and deliveries.
webhooks:writeManage webhook configuration and deliveries.
payments:readRead charges, customer credits, affiliate programs, and affiliate payout records.
payments:writeManage charges, customer credits, affiliate programs, and affiliate payout records.
community:readRead community configuration.
community:writeManage community configuration.
store:readRead store settings.
store:writeManage 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.

Refresh a native client's tokens
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.

Revoke a native client's token family
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:

Disconnect the current installation
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.

On this page