Authentication
Authenticate your API requests with OAuth 2.1 + PKCE, an API key, or a session cookie.
Three available methods#
| Method | Use case | Complexity |
|---|---|---|
| OAuth 2.1 + PKCE | Public third-party apps, multi-user integrations | ⭐⭐⭐ |
API key sk_* | Personal scripts, simple automations | ⭐ |
| Session cookie | Internal front-end (your own Syndic+ app) | ⭐⭐ |
OAuth 2.1 with PKCE#
General flow#
Register your client
Dynamic Client Registration endpoint (RFC 7591):
POST /api/oauth/register
Content-Type: application/json
{
"client_name": "My Integration",
"redirect_uris": ["https://my-app.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"token_endpoint_auth_method": "none"
}
Response: client_id (no client_secret with PKCE).
Redirect the user to Syndic+
Build the authorization URL with code_challenge (PKCE S256):
https://app.syndicplus.ca/api/oauth/authorize?
client_id=abc123&
redirect_uri=https://my-app.com/callback&
response_type=code&
scope=read:co-owners write:meetings&
code_challenge=XYZ&
code_challenge_method=S256&
state=random_csrf_token
User grants consent
The user sees a screen listing the requested scopes. After approval, they're redirected to redirect_uri with ?code=...&state=....
Exchange the code for a token
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://my-app.com/callback&
client_id=abc123&
code_verifier=ORIGINAL_CODE_VERIFIER
Response:
{
"access_token": "eyJhbGc...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "xyz...",
"scope": "read:co-owners write:meetings"
}
Use the token
curl https://app.syndicplus.ca/api/mcp \
-H "Authorization: Bearer eyJhbGc..."
Refresh the token#
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=xyz...&
client_id=abc123
Server metadata#
Syndic+ publishes OAuth metadata per RFC 8414:
GET /.well-known/oauth-authorization-server
GET /.well-known/oauth-protected-resource
API keys (sk_*)#
Generate a key#
- Settings → Developer → API keys → New key
- Give it a descriptive name ("Backup script", "Accounting integration")
- Select scopes (
read:*,write:*,admin:*) - Copy the key — it starts with
sk_live_orsk_test_and will not be shown again
Use#
curl https://app.syndicplus.ca/api/mcp \
-H "Authorization: Bearer sk_live_abc123..."
API keys provide full access per their scopes. Never commit them to a public Git repo. Use environment variables.
Revocation#
From Settings → Developer, click an existing key to revoke it. Revocation is immediate.
Session cookie#
For calls from your front-end already signed in to Syndic+, you can just use the session cookie:
fetch("/api/mcp", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", ... })
});
This method only works for apps hosted on the same domain.
Scopes#
| Scope | Description |
|---|---|
read:co-owners | Read co-owner and unit lists |
write:co-owners | Create/modify co-owners and units |
read:meetings | Read meetings, minutes, resolutions |
write:meetings | Create/modify meetings |
read:finances | Read notices, expenses, budgets |
write:finances | Create assessment notices, accounting entries |
admin:organization | Manage board team members |
read:compliance | Read the compliance dashboard |
JWT token security#
- Algorithm: ES256 (ECDSA on P-256)
- Access token lifetime: 1 hour
- Refresh token lifetime: 30 days (rotation on each use)
- Public JWKS:
/.well-known/jwks.json
Common authentication errors#
| Code | Meaning |
|---|---|
NOT_AUTHENTICATED | No token provided or expired |
INVALID_TOKEN | Malformed token or invalid signature |
INSUFFICIENT_SCOPE | Token lacks the required scope |
REVOKED_TOKEN | API key or consent revoked |
RATE_LIMITED | Quota exceeded — see Rate Limits |