OAuth 2.1
Let other apps sign in their users with a Bot-Hosting account, like "Sign in with Discord". The app then acts on each user's account, limited to the scopes they approve. Built on OAuth 2.1 + PKCE, so any standard OAuth client works out of the box.
The flow
Create an app
Register a name + redirect URI in your developer settings. You get a public client_id (no secret).
Send users to authorize
Redirect to /oauth/authorize with PKCE. The user logs in and approves the scopes.
Exchange the code
They come back to your redirect URI with a code. POST it to the token endpoint for an access token.
Call the API as them
Send the token as a Bearer to /api/v1. Refresh it in the background so the session lives on.
Endpoints
Public client, PKCE is the proof, no client secret. Discovery is served too, so a standard OAuth library configures itself from the metadata URL alone.
2 · Get a token
Exchange the code for an access token, proving PKCE with the original code_verifier. The redirect_uri must match the one you registered.
curl -X POST "https://bot-hosting.net/api/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=AUTH_CODE" \ -d "redirect_uri=https://yourapp.com/callback" \ -d "client_id=bhc_your_client_id" \ -d "code_verifier=PKCE_VERIFIER"
{
"token_type": "Bearer",
"access_token": "bho_1a2b3c...",
"refresh_token": "bhr_9z8y7x...",
"expires_in": 604800,
"scope": "deployments:read deployments:power"
}3 · Call the API
Send the bho_ access token as a Bearer to any /api/v1 endpoint the granted scopes allow. The token never grants admin, and always acts as the user who approved it.
curl -H "Authorization: Bearer bho_your_access_token" \
https://bot-hosting.net/api/v1/deployments4 · Refresh
Access tokens last 7 days. Before one expires, or on a 401, swap the refresh token for a fresh pair. The user is not prompted again, so the session lasts up to 30 days.
curl -X POST "https://bot-hosting.net/api/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=bhr_current_refresh" \ -d "client_id=bhc_your_client_id"
Rotation: every refresh returns a new refresh token and revokes the old one. Always overwrite your stored refresh token with the new one, or the next refresh fails. If a refresh returns invalid_grant (expired, revoked, or a stale token), the grant is dead: send the user back through step 1.
Errors
What you get back when something goes wrong, at each step.
On the callback (redirect back to your app)
If the user cancels, they return to your redirect_uri with an error and your state instead of a code:
https://yourapp.com/callback?error=access_denied&state=RANDOM
A bad client_id, redirect_uri, response_type or PKCE is not redirected back (that would be an open redirect). The user sees an error screen on our side and your callback is never called. So if your callback never fires, check those four in your authorize URL.
From the token endpoint (HTTP 400, JSON)
{
"error": "invalid_grant",
"error_description": "authorization code is invalid, expired or already used"
} From an API call (with the access token)
Token lifetimes
Authorization code
10 min
Single-use, PKCE-bound.
Access token
7 days
Bearer for /api/v1 calls.
Refresh token
30 days
Rotated on every use.
PKCE (required)
PKCE with S256 is mandatory. Generate a random verifier, derive the challenge, send the challenge at authorize and the verifier at token time.
verifier = base64url(random(32 bytes)) code_challenge = base64url(sha256(verifier)) # send code_challenge + code_challenge_method=S256 at /oauth/authorize # send code_verifier at /api/oauth/token
Discovery
Standard metadata endpoints (RFC 8414 + RFC 9728). Most OAuth libraries and MCP clients read these and configure themselves, so you rarely hardcode the URLs above.
Scopes
Request only what you need. The user sees and approves each scope on the consent screen, and the token is limited to those. Same catalog as the REST API.
Deployments
Account
Templates
Revocation
A grant can end three ways, and each one immediately invalidates the tokens: the user disconnects your app from their dashboard, you disconnect a user from your app in your developer settings, or the refresh token expires after 30 days. Handle invalid_grant / a persistent 401 by restarting the flow at step 1.
Security
- Store tokens server-side, tied to your user. Never ship them to the browser or a public repo.
- The access token is a bearer secret: store it server-side, and it is revoked instantly the moment the user disconnects your app (no waiting for expiry).
- http redirect URIs are accepted for local / IP testing, but use https in production. PKCE protects the code either way.
- No client secret exists (public client). Anyone can start a flow, but only the redirect URI you registered ever receives a code.
- OAuth tokens never grant admin, whatever scopes are approved.