Scopes
The two OAuth scopes Insy grants, what each one unlocks, how to request them, and why products.read is not what most people assume.
A scope is the permission a user grants your application on the consent screen. Insy has two, and that is the complete list. Each one unlocks exactly one resource endpoint.
scope is required on every authorize request. RFC 6749 makes it optional; Insy does not. A
request without it will not produce an authorization code.
The two scopes
| Scope | Unlocks | Returns |
|---|---|---|
memberships.read |
GET /oauth/memberships |
The memberships flagged active on the user’s account. |
products.read |
GET /oauth/products |
The digital products the user has created as a seller. Communities are not included. |
GET /oauth/userinfo needs no scope at all. Any valid access token can read it, so you can
identify a user without asking for either permission.
memberships.read
Returns every membership whose isActive flag is set. There is no date filter on the query —
isActive is maintained by Insy’s billing and expiry jobs, so check validFrom and validUntil
yourself if you need a hard boundary rather than a flag. The list is not paginated.
{
"memberships": [
{
"id": "3f9a1c02-7b41-4e8d-9a63-5c0e2d81f7ab",
"productType": "community",
"isActive": true,
"validFrom": "2026-01-14T09:12:44.000Z",
"validUntil": "2026-09-14T09:12:44.000Z"
}
]
}
An empty memberships array means the user has no membership flagged active — it does not mean the
request failed.
products.read
Returns the digital products the user created as a seller. Communities are not products in this sense and are never returned — a seller whose catalogue is only paid communities gets an empty array.
{
"products": [
{
"id": "b71e4d90-2a55-4c31-8f0d-6e9a3b12c4df",
"name": "Trading Signals",
"slug": "trading-signals",
"type": "course"
}
]
}
type is one of ebook, course, video, audio, document, consultation or other, and may
be null if the seller never set one.
Requesting scopes
Scopes go in a single scope parameter, separated by a space or a comma — both work, and you
can mix them. In a URL a space is percent-encoded as %20. The examples below use a space,
which is the RFC 6749 convention.
https://api.insy.io/oauth/authorize
?response_type=code
&client_id=oac_9f2c41e7a83b06d5c4e19f27
&redirect_uri=https%3A%2F%2Fshop.example%2Fcallback
&scope=memberships.read%20products.read
&state=1f7c9b2e4a8dconst authorizeUrl = new URL("https://api.insy.io/oauth/authorize");
authorizeUrl.searchParams.set("response_type", "code");
authorizeUrl.searchParams.set("client_id", "oac_9f2c41e7a83b06d5c4e19f27");
authorizeUrl.searchParams.set("redirect_uri", "https://shop.example/callback");
authorizeUrl.searchParams.set("scope", "memberships.read products.read");
authorizeUrl.searchParams.set("state", crypto.randomUUID());
// URLSearchParams encodes the separating space for you.
console.log(authorizeUrl.toString());<script
async
src="https://insy.io/oauth/widget.js"
data-client-id="oac_9f2c41e7a83b06d5c4e19f27"
data-scope="memberships.read products.read"
></script>Check what you were granted
The token response echoes the scopes actually attached to the access token.
{
"access_token": "…",
"refresh_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "memberships.read"
}
Branch on the scope in the response, not on what you asked for. Refreshing a token carries the
same scopes forward; there is no way to widen a token’s scope, so to gain a scope you must send the
user through the authorize flow again.
Unknown or ungranted scopes
If you request a scope that does not exist, the user is redirected back to your redirect_uri with
an error instead of a code:
https://shop.example/callback?error=invalid_scope&state=1f7c9b2e4a8d
Handle error on your callback route the same way you handle code. The state you sent is
returned with the error, so you can still match the response to the attempt that started it.
Calling a resource endpoint with a token that lacks the required scope returns 403:
{
"error": "insufficient_scope",
"error_description": "memberships.read scope required"
}
An expired or revoked token returns 401 with invalid_token. Note that neither response carries a
WWW-Authenticate header, so read the JSON body to tell the two cases apart.
Ask for the minimum
Request only the scopes the feature you are shipping actually needs.
- Signing users in? Ask for the narrowest scope that works.
/oauth/userinfoneeds none, so a pure sign-in integration does not needmemberships.readat all. - Gating content on an active subscription?
memberships.readalone. - Building a creator-facing dashboard over someone’s own catalogue? That is the case
products.readis for.
Every extra scope is one more line on the consent screen. You cannot widen a token later, so it is tempting to request everything up front — resist it, and re-run consent when a feature genuinely needs more.