Sign-in with Syndicate

Last updated: 2026-10-29Let people sign in to your app with their Syndicate account — OAuth 2.1 authorization code with PKCE, scopes, tokens, userinfo and verifying tokens yourself.

Sign-in with Syndicate

People already have a Syndicate account for Resonance, ASAI, Apeiron and the rest of the platform. Your app can let them use it instead of a new password. The flow is OAuth 2.1 authorization code with PKCE.

Register a client

In Dev Studio open Sign-in apps and create an application:

  • Name — shown to the person on the consent screen.
  • Redirect URIs — up to 10, matched exactly. For a native app use a loopback address (http://127.0.0.1/callback): the port is not compared, so you can pick a free one at run time.
  • Scopes — what the app may ever ask for. email and subscription need a reason in your developer application.

You get a client_id (asc_…) immediately. A client secret is shown once, but the token endpoint does not use it: authentication is PKCE only. Treat every client as public and never ship the secret in a browser bundle or an app binary. One account can hold up to 10 active clients.

Endpoints

MethodEndpointPurpose
GEThttps://auth.aperturesyndicate.com/api/v1/sso/authorizeStart sign-in in the browser
POSThttps://auth.aperturesyndicate.com/api/v1/sso/tokenExchange the code, refresh tokens
GEThttps://auth.aperturesyndicate.com/api/v1/sso/userinfoWho signed in, filtered by scope
GEThttps://auth.aperturesyndicate.com/.well-known/jwks.jsonPublic keys for verifying tokens
GEThttps://auth.aperturesyndicate.com/.well-known/oauth-authorization-serverRFC 8414 metadata

The metadata document also lists scopes for AI agents connecting to our MCP server. A client registered in Dev Studio can request only the four scopes below.

Scopes

ScopeAdds to /userinfo
openidsub — the stable account id
profilenickname, avatar_url, profile_url
emailemail, email_verified
subscriptionsubscription — active plan and features, or null

The person's internal username is never exposed — not in /userinfo and not in the token. Link accounts by sub.

1. Send the person to authorize

Generate a random code_verifier (43–128 characters), and its code_challenge = base64url(SHA-256(verifier)). Then redirect:

text
https://auth.aperturesyndicate.com/api/v1/sso/authorize
  ?response_type=code
  &client_id=asc_…
  &redirect_uri=https://yourapp.example/callback
  &scope=openid profile
  &state=<random>
  &code_challenge=<challenge>
  &code_challenge_method=S256
Parameter
response_typeAlways code
client_idFrom Dev Studio
redirect_uriOne of the registered URIs
scopeSpace-separated, within the client's scopes
stateRequired. Check it on the way back
code_challenge_methodOnly S256; plain is refused
promptOptional. none never shows a screen and returns error=login_required if the person is not signed in
screenOptional. register sends a signed-out person to account creation instead of sign-in

A signed-out person signs in first. Then they always see a consent screen with your app's name and the scopes — third-party apps never get a code silently. If they approve, the browser comes back to:

text
https://yourapp.example/callback?code=…&state=…

The code lives 10 minutes and works once. On refusal or failure the redirect carries error instead — login_required or consent_required with prompt=none.

2. Exchange the code

bash
curl -X POST https://auth.aperturesyndicate.com/api/v1/sso/token \
  -H "content-type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "asc_…",
    "code": "…",
    "redirect_uri": "https://yourapp.example/callback",
    "code_verifier": "…"
  }'
json
{
  "access_token": "eyJ…",
  "refresh_token": "…",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "openid profile"
}

The endpoint accepts JSON or application/x-www-form-urlencoded. A form request gets errors in the RFC 6749 shape ({"error":"invalid_grant","error_description":"…"}); a JSON request gets {"error":"…","code":"…"}.

3. Refresh

The access token lives 15 minutes, the refresh token 30 days. Every refresh returns a new refresh token — store it and drop the old one.

json
{ "grant_type": "refresh_token", "client_id": "asc_…", "refresh_token": "…" }

A refresh token works only for the client it was issued to. If the person signs out everywhere, is banned or deletes the account, refresh fails and they have to sign in again.

4. Who signed in

bash
curl https://auth.aperturesyndicate.com/api/v1/sso/userinfo \
  -H "Authorization: Bearer eyJ…"
json
{
  "sub": "5f0c…",
  "nickname": "Nova",
  "avatar_url": "https://…",
  "profile_url": "https://aperturesyndicate.com/u/nova",
  "email": "…",
  "email_verified": true
}

With subscription:

json
"subscription": {
  "plan": "PREMIUM",
  "status": "ACTIVE",
  "features": ["lq"],
  "items": [],
  "current_period_end": "2026-11-29T00:00:00.000Z",
  "cancel_at_period_end": false
}

Check features, not plan — plan is a legacy field. status is CANCELLED when the subscription runs to the end of the period and stops.

Verifying tokens yourself

The access token is an RS256 JWT. Verify it against the JWKS instead of calling /userinfo on every request:

Claim
isshttps://auth.aperturesyndicate.com
audas-third-party and your client_id — reject a token without your client_id
subAccount id
nicknameDisplay name at the time of issue
scopesGranted scopes
exp, iatExpiry and issue time

A token issued to your app opens only /userinfo. It is not a key to the person's playlists, ASAI or anything else on the platform.

Errors

CodeWhereMeans
INVALID_REQUESTbothMissing or malformed parameter, including plain PKCE
INVALID_CLIENTbothUnknown or disabled client_id
INVALID_REDIRECT_URIbothNot registered, or differs from the one used in step 1
auth.state.missingauthorizestate not sent
INVALID_CODEtokenExpired, used, or issued to another client
PKCE_FAILEDtokencode_verifier does not match the challenge
INVALID_SCOPEtokenA scope outside the client's list
INVALID_TOKENtokenRefresh token expired or revoked
INVALID_GRANTtokenRefresh token belongs to another client
ACCOUNT_BANNEDtoken403; all sessions of the account are closed
MISSING_TOKEN, TOKEN_REVOKEDuserinfo401 — no token, or the person signed out
Sign-in with Syndicate | AS Docs