logo
Schedulin Developers

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://localhost is 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

ScopeGrants
posts:readView posts and drafts
posts:writeCreate, edit, and delete posts
tags:read / tags:writeView / manage tags
social-accounts:read / social-accounts:writeView / manage connected social accounts
channels:readView channels
media:read / media:writeView / upload media
analytics:readView analytics
org:readView 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:

  1. An unauthenticated MCP request returns 401 with a WWW-Authenticate header pointing at the resource metadata (https://mcp.schedulin.app/.well-known/oauth-protected-resource, RFC 9728).
  2. That names this authorization server; its metadata lives at https://api.schedulin.app/.well-known/oauth-authorization-server (RFC 8414).
  3. Clients without a client_id register one at POST https://api.schedulin.app/oauth/register (RFC 7591) — public PKCE clients by default (token_endpoint_auth_method: "none"), or confidential with client_secret_post. The same redirect-URI and naming rules as App Clients apply, and the request is rate-limited.
  4. 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": "..."}):

errorMeaning
invalid_clientUnknown client, or wrong/missing client secret
invalid_grantCode/refresh token invalid, expired, reused, or PKCE verifier mismatch
invalid_requestMissing required parameter (e.g. code_verifier for a public client)
invalid_scopeRequested a scope the client isn't registered for
rate_limitedToo 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.