Let users authorize your app to access Bolta on their behalf. Instead of asking users for an API key, redirect them to Bolta, get their consent, and exchange the returned code for a token that works everywhere an API key does.
Go to Settings → Developer Apps and create a new application. You'll be asked for:
On creation you receive a Client ID and Client Secret. The secret is shown once — store it somewhere safe.
When a user wants to connect their Bolta account to your app, redirect them to:
https://bolta.ai/oauth/authorize?client_id={CLIENT_ID}&response_type=code&redirect_uri={REDIRECT_URI}&scope={SCOPES}&state={STATE}client_idYour app's Client ID.
response_typeMust be code.
redirect_uriMust match a registered redirect URL exactly.
scopeSpace-separated list of scopes. Defaults to the app's configured scopes.
stateRandom string echoed back in the callback. Required for CSRF protection.
After the user approves or denies, Bolta redirects to your registered URL with query parameters.
Approved
https://yourapp.com/callback?code=abc123&state=xyzDenied
https://yourapp.com/callback?error=access_denied&state=xyzAlways verify the state parameter matches what you sent. The authorization code is single-use and expires after 10 minutes.
Make a server-side POST request — never from the browser, since it carries your client secret.
curl -X POST https://platty.boltathread.com/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "abc123",
"redirect_uri": "https://yourapp.com/callback",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}'Response
{
"access_token": "...",
"refresh_token": "...",
"token_type": "bearer",
"expires_in": 5184000,
"scope": "posts:read posts:write accounts:read",
"workspace_id": "...",
"user_id": "..."
}Use the access token as a Bearer token — it works on every /api/v1/ endpoint and the MCP server.
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
"https://platty.boltathread.com/api/v1/workspaces/"NEXT_PUBLIC_BOLTA_SERVER_URL in the web app). Production integrations should target https://platty.boltathread.com.grant_type=refresh_token to /oauth/token to get a new access token. Refresh tokens rotate on use.401.invalid_clientClient ID or Client Secret is wrong.
invalid_grantCode is invalid, expired, or already used.
unsupported_grant_typegrant_type is not authorization_code or refresh_token.
access_deniedUser denied the authorization request.
invalid_scopeRequested scope is unknown or not granted to the client.
import express from "express";
import crypto from "crypto";
const app = express();
const CLIENT_ID = process.env.BOLTA_CLIENT_ID!;
const CLIENT_SECRET = process.env.BOLTA_CLIENT_SECRET!;
const REDIRECT_URI = "https://yourapp.com/callback";
app.get("/connect", (req, res) => {
const state = crypto.randomBytes(16).toString("hex");
req.session.oauthState = state;
const params = new URLSearchParams({
client_id: CLIENT_ID,
response_type: "code",
redirect_uri: REDIRECT_URI,
scope: "posts:read posts:write accounts:read",
state,
});
res.redirect("https://bolta.ai/oauth/authorize?" + params);
});
app.get("/callback", async (req, res) => {
const { code, state, error } = req.query;
if (error) return res.status(400).send("Authorization denied");
if (state !== req.session.oauthState) return res.status(403).send("Bad state");
const r = await fetch("https://platty.boltathread.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
const { access_token, refresh_token, workspace_id } = await r.json();
// Store these somewhere durable, scoped to the user.
res.json({ connected: true, workspace_id });
});