Authentication

Secure your integration with API keys and short-lived JWTs.

Overview

Every Connect API request must include a valid credential. Scoperly supports two authentication methods:

  1. API Key (Direct) — pass your secret key as a Bearer token. Simple, no token exchange needed.
  2. JWT Exchange — exchange your API key for a short-lived JWT. Use when you need expiry control or want to avoid sending the key with every request.

Important: API keys carry full access to your tenant. Keep them secret. Never expose them in client-side code, public repositories, or browser requests.

API Key Format

All Scoperly Connect keys use the sck_live_ prefix and operate on live data — there is no sandbox environment. Test carefully against a non-production tenant of your own if you need a safe playground.

header
Authorization: Bearer sck_live_a1b2c3d4e5f6...

Token Exchange

POST/connect/v1/auth/tokenscope: (API key in header)

Exchange an API key for a short-lived JWT access token.

Response Body

ParameterTypeDescription
access_tokenstringJWT access token. Include as Bearer token in subsequent requests.
token_typestringAlways "Bearer".
expires_inintegerAccess-token lifetime in seconds (1800 = 30 minutes).
refresh_tokenstringOpaque refresh token (crt_*, 30-day nominal expiry). Use to get a new access_token without re-sending your API key. See "Refresh flow" below.
Token Exchange
curl -X POST https://api.scoperly.com/connect/v1/auth/token \
  -H "Authorization: Bearer sck_live_abc123def456..." \
  -H "Content-Type: application/json"

# Response:
# {
#   "access_token": "eyJ...xxxxx",
#   "token_type": "Bearer",
#   "expires_in": 1800,
#   "refresh_token": "crt_..."
# }

Refresh Flow

POST/connect/v1/auth/refreshscope: (refresh token in body)

Exchange a refresh token for a fresh access token. Rotates the refresh token: the old one is invalidated and a new one is returned.

The response has the same shape as /auth/token (access_token, token_type, expires_in, refresh_token). The refresh token rotates on every use — always store the newly returned refresh_token; the one you just sent is invalidated. Each refresh token nominally lives 30 days, but the whole session is hard-capped at 7 days from the original /auth/token exchange (see below) — after that, refresh returns 401 and you must re-exchange the API key.

Refresh
curl -X POST https://api.scoperly.com/connect/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{ "refresh_token": "crt_..." }'

# Response:
# {
#   "access_token": "eyJ...xxxxx",
#   "token_type": "Bearer",
#   "expires_in": 1800,
#   "refresh_token": "crt_...(NEW — replaces the one you sent)"
# }

Scopes

API keys can be scoped to limit their access. When creating a key, select only the scopes your integration needs.

ScopePermits
catalog:readRead products, search catalog, view sync history
catalog:writeCreate, update, delete products, manage stock (single + batch), start/end sync sessions
orders:readList and view order details (incl. internalPartner block)
orders:writeAccept, reject, cancel, dispatch orders (single + batch up to 50)

Verifying which account a key belongs to

GET/connect/v1/mescope: any valid credential

Returns the tenant and key behind the credential you just presented. No scope required — any valid credential may ask who it is.

Call this once during setup, before you push any catalog data or consume any orders, and check that side is SUPPLIER and that tenantSlug is the account the operator expected. An integration configured with the wrong key otherwise discovers the mistake only by observing the damage afterwards.

curl https://api.scoperly.com/connect/v1/me \
  -H "Authorization: Bearer $TOKEN"

# Response:
# {
#   "tenantId": 118,
#   "tenantUuid": "9c1b1f2e-...-...-...-...",
#   "tenantSlug": "avan-dairy",
#   "tenantName": "Avan Dairy",
#   "legalName": "Avan Dairy LLC",
#   "countryCode": "AM",
#   "side": "SUPPLIER",
#   "scopes": ["catalog:read", "catalog:write", "orders:read", "orders:write"],
#   "keyName": "Studio production",
#   "keyPrefix": "sck_live_a1b2",
#   "planTier": "PROFESSIONAL",
#   "keyExpiresAt": null
# }

Session-age cap (7 days)

The refresh-token flow keeps a session alive across many access-token rotations, but there is a hard 7-day upper bound measured from the original POST /auth/token call. After that window, even a valid refresh_token returns 401 — your integration must re-exchange its API key (sck_live_*) for a fresh session.

This cap is server-enforced and not client-configurable. It limits the blast radius of a stolen refresh token: even with the token in hand, an attacker cannot pivot a session beyond 7 days without also compromising the API key.

Plan for this in long-running ETL scripts: catch 401 on /auth/refresh and transparently fall back to /auth/token with your stored API key, rather than alerting on "session expired".

Key Rotation

We recommend rotating keys periodically. Scoperly supports zero-downtime rotation:

  1. Create a new key using the API key management endpoints.
  2. Update your integration to use the new key.
  3. Verify traffic is flowing on the new key (check the “Last used” timestamp).
  4. Revoke the old key once no traffic remains.

Tip: Multiple keys can be active at the same time, so you can run the old and new key simultaneously during rotation.

Idempotency

For mutating endpoints (POST, PUT, DELETE), include an Idempotency-Key header to ensure safe retries. If the server has already processed a request with the same key, it returns the cached response instead of re-executing.

header
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

Keys are scoped to your tenant + API key. Use UUID v4 for maximum uniqueness. Keys are retained for 48 hours, after which a reused key is treated as a new request. Omitting the header disables idempotency protection.

The replayed response includes header Idempotent-Replayed: true so you can distinguish cached responses from fresh ones.

The key is bound to the request body. Retrying the identical request replays the stored response; sending a different body under a key you already used is rejected with 409 IDEMPOTENCY_KEY_REUSED rather than silently answered with the first response. Use a fresh key per distinct request.