Endpoint reference
Every Sign in with Insy endpoint — parameters, request and response examples, error codes, and the places Insy deviates from the OAuth RFCs.
All OAuth endpoints live under https://api.insy.io. There is no version prefix in the path and
no sandbox environment — you develop against production with your own test account.
The OAuth token, revoke, userinfo, memberships and products endpoints return bare JSON, not
the success / data envelope used by the rest of the Insy API. GET /oauth/client/:clientId is
the one exception and does use the envelope.
GET /oauth/authorize
Starts a sign-in. Browser-facing: it always answers with a 302, or with a 400 when it refuses to redirect or the request does not validate.
Auth: none. The user authenticates in the browser during this request.
| Parameter | Type | Required | Description |
|---|---|---|---|
response_type |
string | No | Accepted but not enforced — the server never validates it and always treats the flow as code. Send code for spec compliance. |
client_id |
string | Yes | Your client identifier, oac_ plus 24 hex characters. |
redirect_uri |
string | Yes | Must exactly match a redirect URI registered for the client. |
scope |
string | Yes | Space- or comma-separated. memberships.read, products.read, or both. |
state |
string | No | Opaque value echoed back on the redirect. Max 1024 characters. Strongly recommended. |
code_challenge |
string | Public clients | base64url(SHA-256(code_verifier)). Optional for confidential clients. |
code_challenge_method |
string | No | Optional — S256 is assumed whenever a code_challenge is present. Only S256 is accepted; plain is rejected. |
GET https://api.insy.io/oauth/authorize
?response_type=code
&client_id=oac_9f2c41ab77e05d3612b8c4ef
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Finsy%2Fcallback
&scope=memberships.read%20products.read
&state=7b1e4c9a2f6d8035
Location: https://app.example.com/auth/insy/callback
?code=c4f81a30d97b6e25f0a3c8d17b492e6a
&state=7b1e4c9a2f6d8035
Errors
| Condition | Result |
|---|---|
| User declines the consent screen | 302 to redirect_uri?error=access_denied&state=... |
| Unknown scope requested | 302 to redirect_uri?error=invalid_scope&state=... |
Unknown client_id |
HTTP 400 JSON. Deliberately not redirected. |
redirect_uri not registered for the client |
HTTP 400 JSON. Deliberately not redirected. |
Public client without code_challenge |
HTTP 400 JSON, invalid_request. Checked before the redirect URI is trusted, so it is not redirected. |
Missing scope, state over 1024 characters, code_challenge outside 43–128 characters, code_challenge_method other than S256 |
HTTP 400 JSON, invalid_request. Not redirected. |
Authorization codes are single-use and expire after 60 seconds.
POST /oauth/token
Exchanges an authorization code for tokens, or trades a refresh token for a new pair.
Auth: client credentials in the request body. Accepts application/json or
application/x-www-form-urlencoded.
| Field | Type | Required | Description |
|---|---|---|---|
grant_type |
string | Yes | authorization_code or refresh_token. |
client_id |
string | Yes | Your client identifier. |
client_secret |
string | Confidential clients | ocs_ plus 48 hex characters. Public clients must omit it — sending one returns invalid_client. |
code |
string | For authorization_code |
The code from the authorize redirect. |
redirect_uri |
string | For authorization_code |
The same value sent to /oauth/authorize. |
code_verifier |
string | With PKCE | 43–128 characters. Required whenever the authorization request carried a code_challenge. |
refresh_token |
string | For refresh_token |
The refresh token from a previous response. |
curl -i -X POST https://api.insy.io/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"client_id": "oac_9f2c41ab77e05d3612b8c4ef",
"client_secret": "ocs_3b71e0d9c85a24f6b0d78e13a45c9f27e6b18d04a7c35f92",
"code": "c4f81a30d97b6e25f0a3c8d17b492e6a",
"redirect_uri": "https://app.example.com/auth/insy/callback"
}'curl -i -X POST https://api.insy.io/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=refresh_token \
-d client_id=oac_9f2c41ab77e05d3612b8c4ef \
-d client_secret=ocs_3b71e0d9c85a24f6b0d78e13a45c9f27e6b18d04a7c35f92 \
-d refresh_token=5e2c9a71d84f0b36c7a1e93f5b820d4ec6a37f18{
"access_token": "a1b7c3d9e5f20486b3c1d7e9f5a20486b3c1d7e9",
"refresh_token": "5e2c9a71d84f0b36c7a1e93f5b820d4ec6a37f18",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "memberships.read products.read"
}
Errors
{
"error": "invalid_grant",
"error_description": "Authorization code is invalid or expired"
}
invalid_client is returned with HTTP 401. Every other token error is HTTP 400.
Lifetimes: access token 1 hour, refresh token 30 days on a sliding window. A refresh issues a new pair and immediately revokes both the old refresh token and the old access token. Tokens are opaque random strings — do not parse them.
POST /oauth/revoke
Revokes an access token or a refresh token.
Auth: client credentials in the request body.
| Field | Type | Required | Description |
|---|---|---|---|
client_id |
string | Yes | Your client identifier. |
client_secret |
string | Yes | Required on this endpoint even for public clients — but a public client’s secret is never accepted, so /oauth/revoke is confidential-clients-only today. |
token |
string | Yes | The token to revoke. |
token_type_hint |
string | No | access_token or refresh_token. |
curl -i -X POST https://api.insy.io/oauth/revoke \
-H "Content-Type: application/json" \
-d '{
"client_id": "oac_9f2c41ab77e05d3612b8c4ef",
"client_secret": "ocs_3b71e0d9c85a24f6b0d78e13a45c9f27e6b18d04a7c35f92",
"token": "5e2c9a71d84f0b36c7a1e93f5b820d4ec6a37f18",
"token_type_hint": "refresh_token"
}'
HTTP/1.1 200 OK
Client authentication is checked first: bad or missing credentials answer 401 with
{"error": "invalid_client"} and no error_description. Once the client is authenticated, the
response body is empty and the status is 200 whether or not the token existed. A 200 tells you the
token is not usable — it is not confirmation that it ever was.
GET /oauth/userinfo
The identity of the signed-in user. This is the endpoint to call first, because sub is the value
you key your own records on.
Auth: Authorization: Bearer <access_token>. No scope required.
curl https://api.insy.io/oauth/userinfo \
-H "Authorization: Bearer a1b7c3d9e5f20486b3c1d7e9f5a20486b3c1d7e9"
{
"sub": "3f6b1c88-5d2e-4a71-9c40-8b71e2f0a5d3",
"email": "maja@example.com"
}
| Field | Type | Description |
|---|---|---|
sub |
string (uuid) | The Insy user identifier. Stable — store this. |
email |
string or null | The user’s email address. Can be null, so never use it as a key. |
Errors: invalid_token with HTTP 401.
GET /oauth/memberships
The user’s currently active memberships.
Auth: Authorization: Bearer <access_token> with the memberships.read scope.
curl https://api.insy.io/oauth/memberships \
-H "Authorization: Bearer a1b7c3d9e5f20486b3c1d7e9f5a20486b3c1d7e9"
{
"memberships": [
{
"id": "b28d5f0a-91c4-4e7d-8a63-2f5c07be1d94",
"productType": "community",
"isActive": true,
"validFrom": "2026-07-01T00:00:00.000Z",
"validUntil": "2026-09-01T00:00:00.000Z"
}
]
}
| Field | Type | Description |
|---|---|---|
id |
string (uuid) | Membership identifier. |
productType |
string | The kind of product the membership grants access to: community, digital_product, clipping_campaign or agency. |
isActive |
boolean | Always true in this response — only active memberships are returned. |
validFrom |
string (ISO-8601) | Start of the current access period. |
validUntil |
string (ISO-8601) | End of the current access period. Absent or null for lifetime access. |
Only currently-active memberships appear, so an empty array means “no access right now”, not “never had access”. The list is not paginated.
Errors: invalid_token with HTTP 401, insufficient_scope with HTTP 403.
GET /oauth/products
The products the user has created as a seller.
Auth: Authorization: Bearer <access_token> with the products.read scope.
curl https://api.insy.io/oauth/products \
-H "Authorization: Bearer a1b7c3d9e5f20486b3c1d7e9f5a20486b3c1d7e9"
{
"products": [
{
"id": "0c9a4e17-6b3d-4c02-9f81-73ad5e6c2b18",
"name": "Trading Signals",
"slug": "trading-signals",
"type": "course"
}
]
}
| Field | Type | Description |
|---|---|---|
id |
string (uuid) | Product identifier. |
name |
string | Display name. |
slug |
string | URL slug on the storefront. |
type |
string or null | Product type. One of ebook, course, video, audio, document, consultation, other. Can be null for older products. |
Errors: invalid_token with HTTP 401, insufficient_scope with HTTP 403.
GET /oauth/client/{clientId}
The public display identity of a client — name, description and logo. Use it to render your own consent or “connect” screen without hardcoding the branding.
Auth: none. This endpoint is public and unauthenticated.
| Parameter | In | Description |
|---|---|---|
clientId |
path | The client identifier, oac_ plus 24 hex characters. |
widget_origin |
query | Optional. The browser origin asking to receive this client’s authorization code. When present, the response’s data carries a widgetOriginAllowed boolean. Only the login widget’s callback sends it; the consent screen omits it. |
curl https://api.insy.io/oauth/client/oac_9f2c41ab77e05d3612b8c4ef
This is the one OAuth endpoint that uses the standard Insy response envelope:
{
"success": true,
"data": {
"clientId": "oac_9f2c41ab77e05d3612b8c4ef",
"name": "Example Shop",
"description": "Sign in to see your memberships",
"logoUrl": "https://cdn.example.com/logo.png"
}
}
On failure the envelope carries the reason instead:
{
"success": false,
"errorCode": "not_found",
"message": "Unknown client"
}
An unknown clientId answers with HTTP 404 and this envelope; errorCode values are lowercase
snake_case. Keys that are unset are omitted rather than sent as null — a successful response
carries no errorCode, and a failure carries no data.
Error codes
Token and revoke errors, and errors from the resource endpoints, use the RFC shape:
{
"error": "invalid_grant",
"error_description": "Authorization code is invalid or expired"
}
Authorize errors are delivered as query parameters on the redirect instead, with state echoed
back — except for the ones listed above that are refused before the redirect URI is trusted, which
come back as a 400 in the same RFC shape.
error |
HTTP status | Where it appears | Meaning |
|---|---|---|---|
invalid_request |
400 | /oauth/token, /oauth/revoke, /oauth/authorize |
A required parameter is missing or malformed. |
invalid_client |
401 | /oauth/token, /oauth/revoke |
Unknown client, wrong secret, a confidential client that sent no secret, or a public client that sent one. The /oauth/revoke variant carries no error_description. |
invalid_grant |
400 | /oauth/token |
The code or refresh token is expired, already used, revoked, or the code_verifier does not match the challenge. |
invalid_scope |
— | /oauth/authorize (as a redirect parameter) |
A requested scope is not one of memberships.read, products.read. Delivered as a 302 redirect, never a 400 body. |
access_denied |
— | /oauth/authorize (as a redirect parameter) |
The user declined the consent screen. |
invalid_token |
401 | Resource endpoints | The access token is missing, expired, revoked or unknown. |
insufficient_scope |
403 | Resource endpoints | The token is valid but lacks the scope, for example "memberships.read scope required". |
Known deviations from the RFCs
Plan around these rather than assuming a library will cope.
| Deviation | Impact |
|---|---|
POST /oauth/token returns 201, not the RFC 6749 200. |
Strict clients throw on success. Accept any 2xx. |
client_secret_basic is not supported. |
Credentials must go in the body. Configure client_secret_post; most libraries default to Basic. |
No WWW-Authenticate header on 401/403 from resource endpoints. |
Bearer-token middleware that parses that header for the error will see nothing. Branch on the status code and the error field. |
scope is required on /oauth/authorize. |
RFC 6749 makes it optional. An authorize request without a scope fails. |
/oauth/revoke is unusable by public clients. |
RFC 7009 allows an unauthenticated public client to revoke. Insy requires client_secret on the request and rejects any secret presented by a public client, so there is no request a public client can make that succeeds. Discard the token locally and let it expire. |
| No refresh-token reuse detection. | A replayed refresh token fails with invalid_grant and nothing else happens — there is no cascade revocation and no security signal. Coordinate refreshes yourself when several processes share a token. |
| No OpenID Connect. | No id_token, no /.well-known discovery document, no JWKS. Identity comes from GET /oauth/userinfo. |
| No official SDKs. | Every integration is plain HTTP. The examples in these docs are the reference implementation. |