Wind Wind / Docs
Dashboard →

Protocol

Making requests

The inference endpoint — body, response, streaming, feature tagging, idempotency.

POST api.usewind.app/v1/ai/{model} Bearer <access_token>

The model is in the path — any model Wind offers, or one in the app’s allowed_models if you’ve restricted it. The body is OpenAI-shaped.

Calling by feature

POST api.usewind.app/v1/ai/features/{slug} Bearer <access_token>

Same body and response, but the model comes from the feature’s default_model (set in the console) instead of the path. Changing models then needs no deploy. The feature must exist and have a default_model, or the call fails with 400 feature_model_not_configured. If the app sets allowed_models, the resolved model must be in it.

Models

Wind serves OpenAI, Anthropic (Claude) and Google (Gemini) models behind the same request and response shape. The current list, with what Wind pays per token, is public:

curl https://api.usewind.app/v1/models
[{ "id": "claude-sonnet-5-5", "provider": "anthropic",
   "input_cost_per_1k_usd": "0.002000", "output_cost_per_1k_usd": "0.010000" }, …]

Use these ids exactly in the path. Users pay the provider cost plus Wind’s fee and your margin (see Concepts). Models are added and retired over time; a call to a retired model fails with 400 invalid_request, so check the list when choosing models rather than assuming one stays available forever.

GPT-5 and o-series models are reasoning models: they only accept the default temperature, so leave it out for them.

Headers

HeaderRequiredNotes
AuthorizationyesBearer <access_token> — the token is the connection
Idempotency-Keyyesa UUID; replays return the original result and never double-charge. Reusing a key with a different body is 409 idempotency_key_reused
X-Wind-Featurenoa feature slug — analytics bucket + that feature’s guardrails
Content-Typeyesapplication/json

Body

{
  "messages": [{ "role": "user", "content": "Describe this plate." }],
  "max_tokens": 500,
  "temperature": 0.7,
  "stream": false
}

max_tokens is optional. When omitted, the authorization hold’s cap resolves request → feature max_tokens → app default_max_tokens → model ceiling.

Response

The provider completion, plus a wind block:

{
  "id": "chatcmpl-…",
  "choices": [ … ],
  "usage": { "prompt_tokens": 42, "completion_tokens": 130, "total_tokens": 172 },
  "wind": {
    "feature": "plate-scan",
    "provider_cost_mc": 184000,
    "wind_fee_mc": 18400,
    "dev_margin_mc": 27600,
    "user_charge_mc": 230000,
    "allocation_remaining_mc": 476120000
  }
}

Tools

Pass OpenAI-style function tools (plus optional tool_choice and parallel_tool_calls). This works the same for every provider: Wind translates to Anthropic’s and Gemini’s formats and back, so responses always carry OpenAI-shaped tool_calls and finish_reason: "tool_calls".

{
  "messages": [{ "role": "user", "content": "Weather in Oslo?" }],
  "tools": [{
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "Current weather for a city",
      "parameters": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] }
    }
  }]
}

To send results back, append the assistant message unchanged, then one tool message per call:

{ "role": "tool", "tool_call_id": "call_…", "content": "{\"temp_c\": 12}" }

Keep the assistant message exactly as Wind returned it. On Gemini models a tool call can carry extra_content.google.thought_signature, and Gemini rejects the next turn if it’s missing.

parallel_tool_calls: false is honored on OpenAI and Anthropic models and ignored on Gemini. In sandbox mock mode the canned model calls the first (or forced) tool with {} arguments, then answers in text once the last message is a tool result. That’s enough to exercise your tool loop end to end.

Images

Content parts of type image_url take a data: URL or a public http(s) URL. For Anthropic and Gemini models Wind downloads remote images itself (max 5MB each, 20 per request), so the provider never has to reach your host. A URL that can’t be fetched, isn’t an image, or points at a private address is rejected with 400 invalid_request before anything is held or billed.

Streaming

Set "stream": true. Wind proxies the provider’s SSE stream; the wind block arrives as a final event once the response completes and the ledger row is written.

Metering & idempotency

Every call writes one immutable ledger row (see Concepts). A retried request with the same Idempotency-Key returns the original response and is not billed again — use this for network retries and on 502 provider_error.

The response never includes the user’s total balance — only allocation_remaining_mc for this connection.