Skip to content
Insy
For developers
Esc
navigateopen⌘Jpreview
On this page

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.

Was this page helpful?