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
| Param | Type | Required | Notes |
|---|---|---|---|
client_id | string | yes | Your App’s client_id. |
redirect_uri | string | yes | Must exactly match a registered URI. |
response_type | string | yes | code (the only supported value). |
scope | string | yes | Space-separated. Must include openid for an ID token. |
state | string | yes | Opaque; returned unchanged in the redirect. Use it for CSRF protection. |
nonce | string | strongly recommended | Echoed into the ID token. Verify on receipt. |
code_challenge | string | required for public clients | PKCE challenge (base64url-encoded SHA-256 of the verifier). |
code_challenge_method | string | required if code_challenge set | Must be S256. |
provider | string | optional | Pre-select a sign-in provider. Skips Synq’s chooser. |
prompt | string | optional | none, login, consent, select_account. |
login_hint | string | optional | Pre-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_required—prompt=nonebut no session.consent_required—prompt=nonebut 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_secretform 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 clientsResponse
{
"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 originalResponse
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:
- Revoke their refresh token via
POST /oauth2/revoke. - 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
| Endpoint | Limit |
|---|---|
GET /.well-known/* | 600/min/IP |
GET /oauth2/authorize | 60/min/IP |
POST /oauth2/token | 30/min/client_id |
POST /oauth2/revoke | 30/min/client_id |
POST /oauth2/introspect | 60/min/client_id |
GET /userinfo | 60/min/token |
POST /oauth2/device/authorize | 30/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.