Skip to Content
API ReferenceOIDC

OIDC endpoints

The full OpenID Connect issuer surface. Issuer URL: https://synq.id in production, https://preview-api.synq.id in preview.

Every endpoint follows the relevant RFC. This page documents what Synq actually serves, including the bits the RFCs leave to implementers.

Discovery

GET /.well-known/openid-configuration

Returns the OIDC discovery document. Cache the result for at least 1 hour; metadata changes rarely.

Response

{ "issuer": "https://synq.id", "authorization_endpoint": "https://synq.id/oauth2/authorize", "token_endpoint": "https://synq.id/oauth2/token", "userinfo_endpoint": "https://synq.id/userinfo", "revocation_endpoint": "https://synq.id/oauth2/revoke", "introspection_endpoint": "https://synq.id/oauth2/introspect", "jwks_uri": "https://synq.id/.well-known/jwks.json", "device_authorization_endpoint": "https://synq.id/oauth2/device/authorize", "response_types_supported": ["code"], "grant_types_supported": [ "authorization_code", "refresh_token", "client_credentials", "urn:ietf:params:oauth:grant-type:device_code" ], "code_challenge_methods_supported": ["S256"], "id_token_signing_alg_values_supported": ["RS256"], "token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post" ], "scopes_supported": ["openid", "profile", "email", "offline_access"], "subject_types_supported": ["public"], "claims_supported": [ "sub", "iss", "aud", "exp", "iat", "auth_time", "nonce", "email", "email_verified", "given_name", "family_name", "preferred_username", "picture", "locale" ] }

JWKS

GET /.well-known/jwks.json

Returns the JSON Web Key Set used to verify access and ID tokens. Cache by kid (key id) with a TTL of at least 10 minutes; refetch on a kid cache miss.

Response

{ "keys": [ { "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "synq-2026-q2", "n": "<base64url modulus>", "e": "AQAB" } ] }

Key rotation: Synq adds new keys periodically; old keys remain in the document until every token signed by them has expired. Your verifier should handle this transparently.

Authorization

GET /oauth2/authorize

Starts the authorization-code flow. Sends the user through Synq’s sign-in surface and back to your redirect_uri with an auth code.

Query parameters

ParamTypeRequiredNotes
client_idstringyesYour App’s client_id.
redirect_uristringyesMust exactly match a registered URI.
response_typestringyescode (the only supported value).
scopestringyesSpace-separated. Must include openid for an ID token.
statestringyesOpaque; returned unchanged in the redirect. Use it for CSRF protection.
noncestringstrongly recommendedEchoed into the ID token. Verify on receipt.
code_challengestringrequired for public clientsPKCE challenge (base64url-encoded SHA-256 of the verifier).
code_challenge_methodstringrequired if code_challenge setMust be S256.
providerstringoptionalPre-select a sign-in provider. Skips Synq’s chooser.
promptstringoptionalnone, login, consent, select_account.
login_hintstringoptionalPre-fills the email or username on the sign-in surface.

Success response

302 Found, redirecting to your redirect_uri with:

<redirect_uri>?code=<auth_code>&state=<your state>

Auth code lifetime: 60 seconds. Exchange immediately.

Error response

302 Found, redirecting to your redirect_uri with (RFC 6749 §4.1.2.1):

<redirect_uri>?error=<error code>&error_description=<message>&state=<your state>

Common error codes:

  • invalid_request — bad parameter shape.
  • invalid_scope — scope not allowed for this App.
  • unauthorized_client — App not allowed to use this grant type.
  • access_denied — user denied consent.
  • login_requiredprompt=none but no session.
  • consent_requiredprompt=none but missing consent.

Token

POST /oauth2/token

Form-encoded body. Authenticate confidential clients via either:

  • HTTP Basic: Authorization: Basic base64(client_id:client_secret).
  • Body: client_id + client_secret form fields.

Pick one; don’t send both.

Grant: authorization_code

grant_type=authorization_code &code=<auth_code> &redirect_uri=<the same uri used at /authorize> &client_id=<client_id> &client_secret=<client_secret> # confidential clients &code_verifier=<pkce verifier> # public clients

Response

{ "access_token": "<RS256 JWT>", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "<opaque>", "id_token": "<RS256 JWT>", "scope": "openid profile email offline_access" }

The refresh_token is included only when the granted scope contained offline_access.

Grant: refresh_token

grant_type=refresh_token &refresh_token=<token> &client_id=<client_id> &client_secret=<client_secret> # confidential clients &scope=<optional reduced scope> # subset of original

Response

Same shape as authorization_code. The previous refresh token is consumed; the new one replaces it. Single-use.

If you don’t pass scope, the original scopes are preserved.

Grant: client_credentials

grant_type=client_credentials &client_id=<client_id> &client_secret=<client_secret> &scope=<requested scopes>

Response

{ "access_token": "<RS256 JWT>", "token_type": "Bearer", "expires_in": 3600, "scope": "wallets:read" }

No refresh_token, no id_token (m2m has no user).

Grant: urn:ietf:params:oauth:grant-type:device_code

grant_type=urn:ietf:params:oauth:grant-type:device_code &device_code=<from /oauth2/device/authorize> &client_id=<client_id>

Response (success)

Same shape as authorization_code (sans id_token if openid wasn’t requested).

Response (pending)

{ "error": "authorization_pending" }

Keep polling at interval seconds.

Response (slow down)

{ "error": "slow_down" }

Increase your polling interval by 5 seconds.

Response (terminal)

{ "error": "expired_token" }

device_code expired (10 min). Restart the device flow.

{ "error": "access_denied" }

User denied the request.

Errors (any grant)

RFC 6749 §5.2 envelope:

{ "error": "invalid_grant", "error_description": "Authorization code expired" }

See Errors for the full code catalog.

Revoke

POST /oauth2/revoke

token=<access_token or refresh_token> &token_type_hint=access_token|refresh_token &client_id=<client_id> &client_secret=<client_secret>

Response

200 OK with empty body, regardless of whether the token existed or was already invalid (RFC 7009 §2.2).

This makes revoke safe to call without checking first.

Introspect

POST /oauth2/introspect

token=<token> &token_type_hint=access_token|refresh_token &client_id=<client_id> &client_secret=<client_secret>

Response (active)

{ "active": true, "scope": "openid profile", "client_id": "marketplace-web", "token_type": "Bearer", "exp": 1717000000, "iat": 1716996400, "sub": "user_abc", "iss": "https://synq.id", "aud": "marketplace" }

Response (inactive)

{ "active": false }

When active: false, no other fields are returned.

Prefer local JWT verification for access tokens — no extra round-trip. Use introspection for opaque tokens (API keys) or in environments where JWT verification is impractical.

UserInfo

GET /userinfo

Authorization: Bearer <access_token>

Response

{ "sub": "user_abc", "preferred_username": "kelvin", "given_name": "Kelvin", "family_name": "Del Monte", "picture": "https://files.synq.id/u/abc/pic.png" }

email is not returned even with the email scope — Synq does not persist email on the user record. Read it from the id_token at sign-in, or fetch it from the user’s OAuth provider link if needed.

Device authorization

POST /oauth2/device/authorize

Starts the device flow. Returns a device_code (opaque, for the agent) and a user_code (human-readable, for the user).

Request

client_id=<client_id> &scope=<optional>

Response

{ "device_code": "<opaque>", "user_code": "ABCD-1234", "verification_uri": "https://synq.id/device", "verification_uri_complete": "https://synq.id/device?user_code=ABCD-1234", "expires_in": 600, "interval": 5 }

expires_in is in seconds (10 minutes). interval is the minimum polling interval in seconds; if Synq returns slow_down, add 5 seconds to your interval.

Showing the user

A typical CLI:

✦ Visit https://synq.id/device and enter code: ABCD-1234 (or open this URL directly: https://synq.id/device?user_code=ABCD-1234)

Then poll /oauth2/token with device_code every interval seconds.

Device verification (Synq UI)

GET /device?user_code=<optional> — the page the user visits to enter the user_code and approve the device.

POST /device/approve and POST /device/deny — called by Synq’s own page, not by your app. Documented for completeness.

End session (logout)

OIDC RP-Initiated Logout is not currently exposed at a standard end_session_endpoint. To sign a user out:

  1. Revoke their refresh token via POST /oauth2/revoke.
  2. Destroy your app’s session cookie.

The access token will expire on its own within 1 hour. We are shipping a real end_session_endpoint in the next release; this page will update.

Rate limits

EndpointLimit
GET /.well-known/*600/min/IP
GET /oauth2/authorize60/min/IP
POST /oauth2/token30/min/client_id
POST /oauth2/revoke30/min/client_id
POST /oauth2/introspect60/min/client_id
GET /userinfo60/min/token
POST /oauth2/device/authorize30/min/client_id
POST /oauth2/token (device_code polling)1 per interval seconds/device_code

Responses to rate-limited requests include RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset, and Retry-After.

All errors at a glance

See Errors for the full catalog including non-OIDC REST envelopes.

Last updated on