Build an integration.
Ship an app that calls localhostdevs bots on behalf of other users. The Connect flow is an OAuth-style consent dance: the user authorizes your app once, you get a long-lived token you can use to dispatch commands as them.
When you want this
Connect is the right call for partner apps — anything that calls bots as someone other than yourself.
- Desktop app, IDE extension, or CLI that ships to users
- SaaS that calls a localhostdevs bot for each of its customers
- MCP server you publish for Claude Desktop / Cursor
- Calling a bot from your own server/cron — see /docs/api
- One-off scripts or experiments
- You ARE the only user of your code
The flow at a glance
Three steps. The user clicks something in your app → they're on www.localhostdevs.com authorizing it → you get a token to call the API as them.
1. Redirect the user to /connect
Send the user to https://www.localhostdevs.com/connect with three query params.
https://www.localhostdevs.com/connect
?handle=<the user's localhostdevs handle>
&app=<your app's display name>
&return=<absolute URL on your side that handles the code>Parameters
| Param | Type | Notes |
|---|---|---|
| handle | string (required) | The localhostdevs handle (slug) of the user you want to connect. They'll see "Sign in as @<handle>" if they're not already. |
| app | string (required) | Your app's display name. Shown on the consent screen ("MyApp wants to connect") and saved with the token so the user can identify it in /account/tokens later. |
| return | absolute URL (required) | Where we redirect the browser after Allow/Deny. Must be a full URL (https:// or http://localhost:… in dev). Append your own state in the query string if you need it. |
What the user sees
A small consent card with your app name, a short description of the permissions, and two buttons:
- Allow → we generate a single-use code and redirect them to your
returnURL with?code=…appended. - Deny → we redirect them to your
returnURL with?error=denied.
2. Exchange the code for a token
Your backend (NOT the browser) POSTs the code to /api/v1/auth/exchange and gets back a long-lived app token.
curl -X POST https://www.localhostdevs.com/api/v1/auth/exchange \
-H "Content-Type: application/json" \
-d '{"code":"<code from the redirect>","app":"<your app name>"}'Response
{
"ok": true,
"token": "lhdapp_XXXXXXXXXXXXXXXX...",
"userId": "01J...",
"userHandle": "qa"
}app name the user consented to — NOT the value you pass to exchange — so a malicious integrator can't relabel the connection after the fact. You should still pass the same name on both sides for clarity.Error cases
401 unauthorized— code is unknown, already-redeemed, or expired (5 min).400 validation_error— body is missingcodeorapp.
3. Store the token, then use it
The lhdapp_ token is just an Authorization: Bearer header — call any /api/v1/* endpoint the same way you would with a personal API key.
// Store securely — these are scoped to a single user × app pair.
await db.appTokens.insert({ userId, token, app: 'MyApp' });
// Later, when MyApp wants to dispatch on behalf of this user:
const res = await fetch(
`https://www.localhostdevs.com/api/v1/bots/${handle}/dispatch`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ command: 'ping', args: {} }),
},
);Storage rules
- Tokens are long-lived — they don't expire on a schedule. They die when the user revokes them from their /account/tokens page, or when your code calls
DELETE /api/v1/app-tokens/{id}(coming soon). - Store server-side, encrypted at rest. Never put a token in browser storage / localStorage — anything that runs in the user's browser tab can read it.
- Detect revocation lazily: if any API call returns
401 unauthorized, throw the token away and re-run the Connect flow.
End-to-end example (Node + Express)
A minimal partner-app integration: kick off /connect, accept the callback, exchange, store, use.
import express from 'express';
import { randomUUID } from 'node:crypto';
const app = express();
const APP_NAME = 'MyApp';
const RETURN_URL = 'https://myapp.example.com/connect/callback';
// 1. Start the flow. User clicks "Connect to localhostdevs" in your UI.
app.get('/connect/start', (req, res) => {
const handle = req.query.handle; // collected from the user's profile / form
const state = randomUUID(); // CSRF guard — store this in their session
req.session.connectState = state;
const params = new URLSearchParams({
handle,
app: APP_NAME,
return: `${RETURN_URL}?state=${state}`,
});
res.redirect(`https://www.localhostdevs.com/connect?${params}`);
});
// 2. Handle the callback. Code in the URL, exchange it server-side.
app.get('/connect/callback', async (req, res) => {
if (req.query.state !== req.session.connectState) {
return res.status(400).send('state mismatch');
}
if (req.query.error === 'denied') {
return res.send('User declined.');
}
const code = req.query.code;
if (!code) return res.status(400).send('missing code');
const r = await fetch('https://www.localhostdevs.com/api/v1/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code, app: APP_NAME }),
});
const data = await r.json();
if (!data.ok) return res.status(500).send('exchange failed');
// 3. Persist. Now you have a token good for calling bots as this user.
await db.appTokens.insert({
appUserId: req.session.userId,
lhdUserId: data.userId,
lhdToken: data.token,
});
res.redirect('/dashboard');
});What happens when a user revokes
Users see connected apps on their /account/tokens page and can disconnect any time.
When they click Disconnect:
- The token is marked revoked in our DB — every subsequent API call returns 401.
- We don't webhook your app. You discover the revocation on the next 401 and re-run the Connect flow (or, more politely, prompt the user before doing so).
- Tokens are per-user × per-app. Revoking MyApp doesn't affect any other connections the user has.