Wind Wind / Docs
Dashboard →

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.

GET 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

POST 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

POST 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

POST 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 an Idempotency-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 an increase_url), 403 connection_revoked (terminal: clear the tokens, prompt reconnect) and 403 connection_frozen (paused: show the user the error’s review_url so they can resume it). Full table: Errors & types.
  • Webhooks — optional but recommended: verify Wind-Signature, then react to connection.exhausted / connection.revoked / allocation_change.approved. See Webhooks. Until you add them, poll GET /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.