Integrate
REST API integration
The complete connect flow without an SDK, in six steps.
This guide implements Connect with Wind against the HTTP API directly, with no dependency. It is intended for languages without an SDK, edge runtimes, and integrations that require full control of the flow. The Wind SDK is a convenience layer over the same protocol.
The examples use Node. The steps are identical in any environment that provides a
CSPRNG, SHA-256, base64url encoding, HTTP Basic authentication, and an HTTP
client. This guide assumes an application with a client_id and client_secret,
still in sandbox, where you connect as one of your test accounts (see Quickstart).
WIND_CLIENT_ID=app_xxx
WIND_CLIENT_SECRET=wsk_xxx # server-only, never in a client bundle
WIND_REDIRECT_URI=https://yourapp.com/wind/callback
Money is integer micro-cents, USD. 100_000_000 mc = $1.00 (1¢ = 1_000_000 mc).
1 · Redirect the user to Wind
On a “Connect with Wind” click, build PKCE + state, stash them against the
session, and redirect.
accounts.usewind.app/oauth/authorize browser redirect import crypto from 'node:crypto'
const b64url = (b) => b.toString('base64url')
app.get('/wind/start', (req, res) => {
const verifier = b64url(crypto.randomBytes(48))
const challenge = b64url(crypto.createHash('sha256').update(verifier).digest())
const state = b64url(crypto.randomBytes(24))
req.session.wind = { verifier, state } // MUST persist — verified on callback
const q = new URLSearchParams({
response_type: 'code',
client_id: process.env.WIND_CLIENT_ID,
redirect_uri: process.env.WIND_REDIRECT_URI,
code_challenge: challenge,
code_challenge_method: 'S256',
state,
requested_allocation_mc: '1000000000', // optional — $10 suggestion the user edits
features: 'chat,summarize', // optional — names the consent "what for" line
})
res.redirect(`https://accounts.usewind.app/oauth/authorize?${q}`)
})
The user logs into Wind, sets a spend cap on the consent screen, and approves.
Wind redirects to WIND_REDIRECT_URI with ?code=...&state=.... The code is
single-use, ~60 s TTL.
If the user clicks Deny, Wind redirects to the same URI with
?error=access_denied&state=... and no code. Handle it as a normal outcome:
check state, then send the user back into your app (step 2 shows it).
2 · Exchange the code — server-side
api.usewind.app/oauth/token Basic client_id:client_secret app.get('/wind/callback', async (req, res) => {
const { code, state, error } = req.query
const saved = req.session.wind
if (!saved || state !== saved.state) return res.status(400).send('bad state')
delete req.session.wind
// The user clicked Deny — not an error. Nothing to exchange or store.
if (error === 'access_denied') return res.redirect('/settings?wind=denied')
if (error) return res.status(400).send(`Wind connect failed: ${error}`)
const r = await fetch('https://api.usewind.app/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Basic ' + Buffer.from(
`${process.env.WIND_CLIENT_ID}:${process.env.WIND_CLIENT_SECRET}`,
).toString('base64'),
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: process.env.WIND_REDIRECT_URI, // must match step 1 exactly
code_verifier: saved.verifier,
}),
})
if (!r.ok) return res.status(502).send('token exchange failed')
const t = await r.json()
// { access_token, expires_in, refresh_token, connection_id, allocation_mc }
await persistWind(currentUserId(req), t)
res.redirect('/settings?wind=connected')
})
3 · Store the refresh token securely
async function persistWind(userId, t) {
await db.users.update(userId, {
windConnectionId: t.connection_id,
windRefreshToken: t.refresh_token, // rotates on every use
windAccessToken: t.access_token,
windAccessExpiresAt: Date.now() + t.expires_in * 1000,
})
}
Rotation is atomic or bust
Every refresh returns a new refresh_token and
invalidates the old one. Persist the new one in the same transaction as whatever
triggered the refresh. A crash in between locks the user out — they must
re-connect.
4 · Mint / refresh access tokens
api.usewind.app/oauth/token Basic client_id:client_secret async function windAccessToken(user) {
if (user.windAccessToken && user.windAccessExpiresAt > Date.now() + 60_000) {
return user.windAccessToken
}
const r = await fetch('https://api.usewind.app/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Basic ' + Buffer.from(
`${process.env.WIND_CLIENT_ID}:${process.env.WIND_CLIENT_SECRET}`,
).toString('base64'),
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: user.windRefreshToken,
}),
})
if (!r.ok) throw new Error('wind refresh failed — user must reconnect')
const t = await r.json()
await db.users.update(user.id, { // store the NEW refresh token
windRefreshToken: t.refresh_token,
windAccessToken: t.access_token,
windAccessExpiresAt: Date.now() + t.expires_in * 1000,
})
return t.access_token
}
5 · Send billed requests
api.usewind.app/v1/ai/{model} Bearer <access_token> Model in the path (any model Wind offers, or one in allowed_models if the app sets it); OpenAI-shaped body.
async function windChat(user, { model = 'gpt-4o', messages, max_tokens }) {
const token = await windAccessToken(user)
const r = await fetch(`https://api.usewind.app/v1/ai/${model}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Idempotency-Key': crypto.randomUUID(),
'X-Wind-Feature': 'chat', // optional — analytics + guardrails
'Content-Type': 'application/json',
},
body: JSON.stringify({ messages, max_tokens }),
})
const data = await r.json()
if (!r.ok) throw Object.assign(new Error(data.error?.code || 'wind_error'), {
status: r.status, data,
})
return data // data.choices[...] + data.wind.{ user_charge_mc, allocation_remaining_mc, ... }
}
Detail: Making requests (streaming, token accounting, the
wind block).
6 · Idempotency & webhooks
- Idempotency — every
POST /v1/ai/*needs anIdempotency-Key. Reuse the same value to retry (network blip,502 provider_error) without double-charging. - Errors — handle at least
401 unauthorized(refresh once and retry),402 allocation_exhausted(prompt top-up; the error carries anincrease_url),403 connection_revoked(terminal: clear the tokens, prompt reconnect) and403 connection_frozen(paused: show the user the error’sreview_urlso they can resume it). Full table: Errors & types. - Webhooks — optional but recommended: verify
Wind-Signature, then react toconnection.exhausted/connection.revoked/allocation_change.approved. See Webhooks. Until you add them, pollGET /v1/connections.
Other languages
Same six steps. Python: secrets.token_urlsafe, hashlib.sha256,
base64.urlsafe_b64encode, requests/httpx with HTTP Basic on /oauth/token.
Edge runtimes: Web Crypto crypto.subtle.digest('SHA-256', …) and crypto.getRandomValues.