OAuth for App Clients
API keys authenticate your own workspace. To build an app that lets other Schedulin users connect their accounts, register an App Client and use the OAuth 2.0 authorization-code flow. Users approve exactly which permissions your app receives, and you never see their password.
When to use OAuth vs. API keys
- API key — scripts and integrations acting on your own workspace. Passed as
x-api-key. - OAuth access token — apps acting on behalf of other users' workspaces.
Passed as
Authorization: Bearer <token>.
Both are accepted on the same https://api.schedulin.app/v0 REST API.
Register an app client
Under Settings → API & Apps → App Clients, create a client. You'll provide:
- Name, logo, privacy policy URL — shown to users on the consent screen.
- Permissions (scopes) — the maximum your app can request (see below).
- Redirect URLs — up to 5 exact-match
https://callback URLs (http://localhostis allowed for development). - Public client — enable for apps that can't keep a secret (browser extensions, mobile, SPAs, open-source). Public clients use PKCE instead of a client secret. This choice is permanent.
Confidential clients receive a client secret once at creation — store it securely. You can rotate it any time (the old secret stops working immediately).
Scopes
| Scope | Grants |
|---|---|
posts:read | View posts and drafts |
posts:write | Create, edit, and delete posts |
tags:read / tags:write | View / manage tags |
social-accounts:read / social-accounts:write | View / manage connected social accounts |
channels:read | View channels |
media:read / media:write | View / upload media |
analytics:read | View analytics |
org:read | View organization details |
A token can only call endpoints covered by its granted scopes — anything else
returns 403 Insufficient scope. Request the least you need.
Authorization-code flow
1. Redirect the user to authorize
https://api.schedulin.app/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://yourapp.com/callback
&scope=posts:read%20posts:write
&state=RANDOM_STRING
The user signs in, picks which organization to grant access to, and approves.
Schedulin redirects back to your redirect_uri with ?code=...&state=....
Always verify state matches what you sent.
2. Exchange the code for tokens
Confidential client:
curl -X POST https://api.schedulin.app/oauth/token \ -d grant_type=authorization_code \ -d code=THE_CODE \ -d redirect_uri=https://yourapp.com/callback \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET
{ "access_token": "…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "…", "scope": "posts:read posts:write" }
3. Call the API
curl https://api.schedulin.app/v0/posts \ -H "Authorization: Bearer ACCESS_TOKEN"
4. Refresh when the access token expires
Access tokens last 1 hour; refresh tokens last 90 days and rotate on each use (the old refresh token is invalidated).
curl -X POST https://api.schedulin.app/oauth/token \ -d grant_type=refresh_token \ -d refresh_token=THE_REFRESH_TOKEN \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET # omit for public clients
PKCE (public clients)
Public clients must send a PKCE challenge. Generate a random code_verifier,
derive its S256 challenge, and include it on the authorize request:
CODE_VERIFIER=$(openssl rand -hex 32) CODE_CHALLENGE=$(printf %s "$CODE_VERIFIER" \ | openssl dgst -sha256 -binary | openssl base64 \ | tr '+/' '-_' | tr -d '=')
…/oauth/authorize?…&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256
Then send code_verifier (and no client_secret) at token exchange:
curl -X POST https://api.schedulin.app/oauth/token \ -d grant_type=authorization_code \ -d code=THE_CODE \ -d redirect_uri=https://yourapp.com/callback \ -d client_id=YOUR_CLIENT_ID \ -d code_verifier=$CODE_VERIFIER
Revoking access
curl -X POST https://api.schedulin.app/oauth/revoke \ -d token=ACCESS_OR_REFRESH_TOKEN
Users can also revoke your app any time under Settings → API & Apps → Connected Apps, and a workspace admin can disable or delete the app client — both immediately invalidate all issued tokens.
Discovery & dynamic registration (MCP clients)
The Schedulin MCP server is an OAuth-protected resource. MCP-compatible clients don't need anything pre-registered — the standard discovery chain is fully supported:
- An unauthenticated MCP request returns
401with aWWW-Authenticateheader pointing at the resource metadata (https://mcp.schedulin.app/.well-known/oauth-protected-resource, RFC 9728). - That names this authorization server; its metadata lives at
https://api.schedulin.app/.well-known/oauth-authorization-server(RFC 8414). - Clients without a
client_idregister one atPOST https://api.schedulin.app/oauth/register(RFC 7591) — public PKCE clients by default (token_endpoint_auth_method: "none"), or confidential withclient_secret_post. The same redirect-URI and naming rules as App Clients apply, and the request is rate-limited. - The normal authorization-code + PKCE flow above completes the connection.
This is how hosted agents (ChatGPT, Notion, Perplexity, Claude) connect to Schedulin's MCP server with nothing but its URL.
Error responses
Token/authorize errors follow RFC 6749 ({"error": "...", "error_description": "..."}):
error | Meaning |
|---|---|
invalid_client | Unknown client, or wrong/missing client secret |
invalid_grant | Code/refresh token invalid, expired, reused, or PKCE verifier mismatch |
invalid_request | Missing required parameter (e.g. code_verifier for a public client) |
invalid_scope | Requested a scope the client isn't registered for |
rate_limited | Too many requests — back off and retry |
API calls with an expired or revoked token return 401; calls outside your
granted scopes return 403 Insufficient scope.