OAuth 2.0
OAuth 2.0 is the standard way for third-party applications and AI agents — such as Claude, custom integrations, or partner tools — to act on behalf of SchedPilot users without ever handling their password or personal API key.
When to Use OAuth vs. API Keys
| Scenario | Use |
|---|---|
| Personal script or automation that only you will run | API Key |
| App, integration, or AI agent that other SchedPilot users will authorize | OAuth 2.0 |
| CI/CD pipeline or server-side job you control | API Key |
| Marketplace app or plugin built for public use | OAuth 2.0 |
If you are building something that other people will connect to their SchedPilot accounts, OAuth is the right choice. It gives users full visibility and control — they can see what your app has access to and revoke it at any time.
Flow Overview
SchedPilot implements the Authorization Code flow. The steps are:
- Register your app in the SchedPilot dashboard to get a
client_idandclient_secret. - Redirect the user to the SchedPilot consent page.
- Exchange the authorization code for an access token.
- Call the API using the access token.
Step 1: Register an OAuth App
- Log in to app.schedpilot.com and navigate to API Access.
- Scroll to the OAuth Apps section and click Register New App.
- Fill in:
- App name — displayed to users on the consent screen.
- Redirect URI — the URL SchedPilot will send the user back to after they approve or deny access. Must be an exact match (scheme, host, path, and query string).
- Click Register.
You will receive:
| Value | Prefix | Notes |
|---|---|---|
client_id | sp_cid_ | Safe to include in client-side code. |
client_secret | sp_cs_ | Shown once only. Store it immediately in a secret manager. Never expose it in client-side code or version control. |
Step 2: Redirect the User to Authorize
Send the user to the SchedPilot consent page:
GET https://app.schedpilot.com/oauth/authorize
Query parameters
| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your app's sp_cid_... value. |
redirect_uri | Yes | Must exactly match the URI registered in Step 1. |
response_type | Yes | Must be code. |
state | Recommended | A random, unguessable string. SchedPilot will echo it back to you after the redirect. Use it to prevent CSRF attacks. |
Example authorization URL
https://app.schedpilot.com/oauth/authorize
?client_id=sp_cid_abc123
&redirect_uri=https%3A%2F%2Fmyapp.example.com%2Fauth%2Fcallback
&response_type=code
&state=xK9p2mNqR7sT4uWv
The user will see a consent screen showing your app name and will be asked to approve or deny access.
- Approved: SchedPilot redirects to your
redirect_uriwith?code=AUTH_CODE&state=YOUR_STATE. - Denied: SchedPilot redirects to your
redirect_uriwith?error=access_denied&state=YOUR_STATE.
Step 3: Exchange the Code for an Access Token
After the user approves access, exchange the short-lived code for a long-lived access token. This request must be made server-side so your client_secret is never exposed.
Endpoint
POST https://api.schedpilot.com/developers/v1/oauth/token
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | Must be authorization_code. |
client_id | string | Yes | Your sp_cid_... value. |
client_secret | string | Yes | Your sp_cs_... value. |
code | string | Yes | The code received in the redirect query string. |
Example request
curl -X POST "https://api.schedpilot.com/developers/v1/oauth/token" \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"client_id": "sp_cid_abc123",
"client_secret": "sp_cs_your_secret_here",
"code": "AUTH_CODE_FROM_REDIRECT"
}'
Response
{
"access_token": "sp_tok_xxxxxxxxxxxxxxxxxxxxxxxx",
"token_type": "bearer"
}
Store the access_token securely — treat it with the same care as a password.
Step 4: Use the Access Token
Pass the token in the Authorization header on every SchedPilot API request:
curl "https://api.schedpilot.com/developers/v1/accounts" \
-H "Authorization: Bearer sp_tok_xxxxxxxxxxxxxxxxxxxxxxxx"
The token grants access to all /developers/v1 endpoints on behalf of the user who authorized your app.
Token Lifecycle
- OAuth access tokens do not expire by default. Once issued, a token remains valid until explicitly revoked.
- Users can review all active tokens and revoke them at any time from app.schedpilot.com > API Access > Active OAuth Tokens.
- If your app detects a
401 Unauthorizedor403 Forbiddenresponse with ansp_tok_token, the user has likely revoked it. Restart the OAuth flow to re-authorize.
Complete Node.js + Express Example
The following example shows the full OAuth flow — redirecting the user, handling the callback, and exchanging the code for a token.
const express = require('express');
const crypto = require('crypto');
const fetch = require('node-fetch'); // or use the built-in fetch in Node 18+
const app = express();
const CLIENT_ID = process.env.SCHEDPILOT_CLIENT_ID;
const CLIENT_SECRET = process.env.SCHEDPILOT_CLIENT_SECRET;
const REDIRECT_URI = 'https://myapp.example.com/auth/callback';
// In-memory store for state tokens (use a real store in production)
const pendingStates = new Set();
// Step 2: Redirect the user to the SchedPilot consent page
app.get('/auth/schedpilot', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
pendingStates.add(state);
const params = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
state,
});
res.redirect(`https://app.schedpilot.com/oauth/authorize?${params}`);
});
// Step 3: Handle the redirect and exchange the code for a token
app.get('/auth/callback', async (req, res) => {
const { code, state, error } = req.query;
// User denied access
if (error === 'access_denied') {
return res.status(403).send('Access denied by user.');
}
// Validate the state parameter to prevent CSRF
if (!state || !pendingStates.has(state)) {
return res.status(400).send('Invalid or missing state parameter.');
}
pendingStates.delete(state);
// Exchange the code for an access token
const response = await fetch('https://api.schedpilot.com/developers/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code,
}),
});
if (!response.ok) {
return res.status(500).send('Token exchange failed.');
}
const { access_token } = await response.json();
// Store access_token securely (database, encrypted session, etc.)
// req.session.schedpilotToken = access_token;
res.send('Authorization successful! You can now use SchedPilot on behalf of this user.');
});
// Step 4: Make an API call using the token
app.get('/accounts', async (req, res) => {
// Retrieve the token from wherever you stored it
const token = req.session?.schedpilotToken;
const response = await fetch('https://api.schedpilot.com/developers/v1/accounts', {
headers: { 'Authorization': `Bearer ${token}` },
});
const accounts = await response.json();
res.json(accounts);
});
app.listen(3000, () => console.log('Listening on port 3000'));
Managing OAuth Apps & Tokens
OAuth apps and active tokens are managed entirely through the SchedPilot dashboard — no API calls needed.
Go to app.schedpilot.com/api-access and scroll to the OAuth Apps section to:
- Register a new app (get your
client_idand one-timeclient_secret) - View active token count per app
- Delete an app (revokes all its tokens immediately)
- View and revoke individual active tokens under Active OAuth Tokens
Security Best Practices
| Practice | Why |
|---|---|
Store client_secret server-side only | Exposing it in client-side code allows anyone to impersonate your app. |
Validate the state parameter on every callback | Prevents CSRF attacks that could trick users into authorizing malicious requests. |
Use https:// for your redirect_uri | Prevents the authorization code from being intercepted in transit. |
Store access_token in encrypted storage | Treat it like a password — it grants full API access on the user's behalf. |
Handle 401/403 gracefully | Re-run the OAuth flow if the user has revoked your token. |