Vento e câmeras das praias, no seu sistema.
A API do Windwave entrega o vento medido pelas estações nas praias, o histórico dessas medições e as câmeras ao vivo — para rádios, sites de turismo, escolas, apps e painéis de TV.
Endereço: https://windwave.com.br/api/v1 · Respostas em JSON (UTF-8) · Horários em UTC (ISO 8601) · Vento em nós (kt), direção em graus de onde o vento vem, temperatura em °C.
ww_live_… da Windwave.POST /session.wws_… em todos os pedidos.POST /session/refresh. Ao desligar, DELETE /session.Em 2 minutos
Abra a sessão com a sua chave:
curl -X POST https://windwave.com.br/api/v1/session \
-H "Authorization: Bearer ww_live_SUA_CHAVE"
{
"token": "wws_8fK2…",
"token_type": "Bearer",
"session_id": "7ed31694-…",
"expires_at": "2026-11-07T15:27:22Z",
"expires_in": 900,
"env": "live"
}
Com o token, peça as estações e o vento de agora:
curl https://windwave.com.br/api/v1/stations -H "Authorization: Bearer wws_8fK2…"
curl https://windwave.com.br/api/v1/stations/{id}/now -H "Authorization: Bearer wws_8fK2…"
{
"station_id": "5b1c…",
"at": "2026-11-07T15:12:00Z",
"age_sec": 42,
"wind_kt": 18.4,
"gust_kt": 23.9,
"dir_deg": 105,
"temp_c": 28.1,
"units": { "wind": "kt", "dir": "deg", "temp": "C" }
}
Ainda sem chave de produção? Use uma chave de teste: ela responde com uma estação de demonstração.
Chave e sessão
A chave (ww_live_… ou ww_test_…) é a sua senha de acesso. Ela só serve para abrir a sessão — nunca vai nos outros pedidos. Guarde-a no servidor (variável de ambiente, cofre de segredos), nunca em app de celular, página web ou repositório.
A sessão (wws_…) vale 15 minutos e vai no cabeçalho Authorization: Bearer … de todo pedido.
/session
Abre a sessão. Cabeçalho: Authorization: Bearer ww_live_…. Responde 201 com o token.
/session/refresh
Renova: devolve um token novo com mais 15 minutos, na mesma sessão. O token anterior para de valer na hora — passe a usar o novo. Recomendado a cada 10 minutos. Se a sessão vencer (você ficou mais de 15 min sem renovar), abra outra com a chave.
/session
Mostra a sessão atual (cliente, escopos, validade). Não conta na cota.
/session
Encerra a sessão e libera a chave na hora. Chame ao desligar o seu sistema — assim, se ele subir em outro servidor, não precisa esperar a sessão antiga vencer.
Uma chave, um lugar
Cada chave funciona em um lugar por vez. Isso protege o seu acesso: se a chave vazar, ela não funciona em paralelo em outro sistema.
- "Lugar" é a rede de onde vêm os pedidos (a mesma faixa de IP, /24 no IPv4 e /48 no IPv6). Trocar de IP dentro da mesma rede, como fazem as nuvens, não é problema.
- Se você abrir sessão de novo da mesma rede (o seu sistema reiniciou), a sessão anterior é substituída.
- Se outra rede tentar abrir sessão com a mesma chave enquanto a sua está ativa, recebe
409 key_in_use. - Três tentativas assim em 24 horas suspendem a chave por segurança. Fale com a Windwave para liberar.
- O token de sessão também é preso à rede onde foi aberto: usado de outra rede, responde
401 session_ip_mismatch.
Vai mudar de servidor? Chame DELETE /session no servidor antigo antes, ou espere até 15 minutos para a sessão dele vencer.
Precisa de mais de um lugar (por exemplo, dois servidores em regiões diferentes)? Peça uma chave para cada um, ou uma chave com mais lugares.
Estações de vento
/stations
escopostations:readEstações liberadas para a sua chave. live indica se a estação mandou leitura nos últimos 10 minutos.
{ "data": [ {
"id": "5b1c…", "name": "Cumbuco Norte", "provider": "windwave",
"spot": { "name": "Cumbuco", "uf": "CE", "lat": -3.6256, "lon": -38.7311 },
"live": true, "last_reading_at": "2026-11-07T15:12:00Z"
} ] }
/stations/{id}/now
escopostations:readA leitura mais recente. age_sec diz há quantos segundos ela foi medida — confira antes de exibir como "agora". Sem leituras ainda, at vem null.
| Campo | Significado |
|---|---|
wind_kt | Vento médio, em nós |
gust_kt | Rajada, em nós |
dir_deg | De onde o vento vem, 0 a 359 (90 = leste) |
temp_c | Temperatura do ar, °C (pode vir null) |
Histórico
/stations/{id}/history?from=&to=&interval=
escopohistory:read| Parâmetro | Padrão | Notas |
|---|---|---|
from | 24 h antes de to | ISO 8601, ex. 2026-11-01T00:00:00Z |
to | agora | ISO 8601 |
interval | 1h | 10m (até 7 dias), 1h (até 31 dias), 1d (até 366 dias) |
{ "station_id": "5b1c…", "interval": "1h", "from": "…", "to": "…",
"data": [ { "t": "2026-11-07T14:00:00Z", "wind_avg_kt": 17.2, "wind_max_kt": 20.1,
"gust_max_kt": 24.6, "dir_deg": 102, "temp_c": 28.4, "samples": 60 } ] }
t é o início do intervalo. A direção é a média vetorial (não a média simples dos graus). Intervalos sem medição não aparecem.
Câmeras
/cameras
escopocameras:readCâmeras liberadas para a sua chave, com online indicando se a imagem está chegando agora.
{ "data": [ { "id": "01d4…", "slug": "canoa-quebrada", "name": "Canoa Quebrada",
"spot": { "name": "Canoa Quebrada", "uf": "CE", … }, "online": true, "last_seen": "…" } ] }
Vídeo ao vivo
/cameras/{id}/stream
escopocameras:liveDevolve um endereço de vídeo HLS (.m3u8) que vale 10 minutos. Esse endereço pode ir para o navegador ou app de quem vai assistir — ele não revela a sua chave.
{ "camera_id": "01d4…",
"hls_url": "https://cams.windwave.com.br/cam/canoa-quebrada/index.m3u8?token=wwst_…",
"expires_at": "…", "expires_in": 600, "max_viewers": 1 }
- Telas ao mesmo tempo:
max_viewersé quantos aparelhos diferentes (IPs) podem assistir juntos com a sua chave. Passou disso, o vídeo é recusado para o aparelho novo. - Validade: quem já está assistindo continua; para começar a assistir depois de 10 minutos, peça um endereço novo.
- Para exibir numa página, use hls.js (o Safari toca HLS sem biblioteca):
<video id="v" muted autoplay playsinline></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
<script>
// hlsUrl veio do SEU servidor (que chamou POST /cameras/{id}/stream)
const v = document.getElementById("v");
if (v.canPlayType("application/vnd.apple.mpegurl")) v.src = hlsUrl;
else { const h = new Hls(); h.loadSource(hlsUrl); h.attachMedia(v); }
</script>
O pedido do endereço é feito pelo seu servidor (com a sessão). Nunca coloque a chave nem o token de sessão no navegador.
Uso
/usage
Pedidos do mês, cota, limite por minuto e uso diário dos últimos 30 dias.
/status
sem autenticaçãoPara o seu monitoramento: responde {"status":"ok"} quando a API está no ar.
Limites
Cada chave tem um limite de pedidos por minuto e uma cota por mês (calendário UTC), definidos no seu contrato. Todo pedido com sessão devolve os cabeçalhos:
| Cabeçalho | Significado |
|---|---|
RateLimit-Limit | Pedidos por minuto permitidos |
RateLimit-Remaining | Quantos ainda cabem neste minuto |
RateLimit-Reset | Segundos até o minuto virar |
X-Quota-Limit / X-Quota-Used | Cota do mês e quanto já foi usado |
Passou do limite: 429 rate_limited com Retry-After em segundos, ou 429 quota_exceeded até virar o mês.
Dica: para exibir o vento ao vivo, pedir /now a cada 60 segundos por estação é o suficiente. Pedir mais rápido tende a devolver a mesma leitura e só gasta cota.
Erros
Erros seguem o formato RFC 9457 (application/problem+json). Use o campo code no seu sistema; o title é para pessoas.
{ "type": "https://windwave.com.br/developers#erro-key_in_use",
"title": "Esta chave já está em uso por outro sistema. …",
"status": 409, "code": "key_in_use", "hint": "…" }
| HTTP | code | O que fazer |
|---|---|---|
| 401 | missing_token | Envie Authorization: Bearer … |
| 401 | invalid_key | Chave errada ou incompleta. Confira se copiou inteira. |
| 401 | invalid_session | Token encerrado ou substituído. Abra outra sessão. |
| 401 | session_expired | Passou de 15 min sem renovar. Abra outra sessão. |
| 401 | session_ip_mismatch | Token usado de outra rede. Abra a sessão de onde vai usar. |
| 409 | key_in_use | A chave tem sessão ativa em outro lugar. Veja Uma chave, um lugar. |
| 403 | key_suspended | Chave suspensa. Fale com a Windwave. |
| 403 | key_revoked | Chave cancelada. Use a chave nova. |
| 403 | key_expired | Validade da chave terminou. |
| 403 | ip_not_allowed | A chave só aceita IPs liberados. Peça para incluir o seu. |
| 403 | forbidden | O seu plano não inclui este recurso. |
| 404 | not_found | Recurso não existe ou não está liberado para a chave. |
| 400 | bad_request | Parâmetro inválido; veja detail. |
| 405 | method_not_allowed | Método HTTP errado para a rota. |
| 421 | proxy_required | Use o endereço oficial windwave.com.br/api/v1. |
| 429 | rate_limited | Espere Retry-After segundos. |
| 429 | quota_exceeded | Cota do mês acabou. |
| 429 | stream_limit | Limite de vídeos abertos. |
| 500 | server_error | Tente de novo em alguns segundos, com espera crescente. |
| 503 | not_configured | API em manutenção. Tente mais tarde. |
Chave de teste
Chaves ww_test_… seguem as mesmas regras (sessão, limites, erros), mas nunca devolvem dados reais: a lista de estações traz só demo-station, com vento simulado que muda ao longo do dia, e a lista de câmeras vem vazia. Use para integrar antes de contratar.
GET /stations/demo-station/now
GET /stations/demo-station/history?interval=10m
Cliente completo
Um cliente que abre a sessão, renova sozinho, trata os erros mais comuns e libera a chave ao desligar.
// npm não precisa: Node 18+ tem fetch
const BASE = "https://windwave.com.br/api/v1";
const KEY = process.env.WINDWAVE_KEY; // ww_live_…
let token = null, renewAt = 0;
async function open() {
const r = await fetch(BASE + "/session", { method: "POST", headers: { Authorization: "Bearer " + KEY } });
const j = await r.json();
if (!r.ok) throw Object.assign(new Error(j.title), { code: j.code });
token = j.token; renewAt = Date.now() + 10 * 60e3;
}
async function refresh() {
const r = await fetch(BASE + "/session/refresh", { method: "POST", headers: { Authorization: "Bearer " + token } });
if (!r.ok) return open(); // venceu? abre outra
token = (await r.json()).token; renewAt = Date.now() + 10 * 60e3;
}
async function api(path, init = {}) {
if (!token) await open(); else if (Date.now() > renewAt) await refresh();
let r = await fetch(BASE + path, { ...init, headers: { ...init.headers, Authorization: "Bearer " + token } });
if (r.status === 401) { await open(); r = await fetch(BASE + path, { ...init, headers: { ...init.headers, Authorization: "Bearer " + token } }); }
if (r.status === 429) { const s = +r.headers.get("retry-after") || 60; await new Promise(ok => setTimeout(ok, s * 1000)); return api(path, init); }
const j = await r.json();
if (!r.ok) throw Object.assign(new Error(j.title), { code: j.code });
return j;
}
async function close() { if (token) await fetch(BASE + "/session", { method: "DELETE", headers: { Authorization: "Bearer " + token } }).catch(() => {}); }
process.on("SIGINT", async () => { await close(); process.exit(0); });
process.on("SIGTERM", async () => { await close(); process.exit(0); });
// exemplo: atualiza o vento a cada minuto
const { data } = await api("/stations");
setInterval(async () => {
for (const s of data) { const now = await api(`/stations/${s.id}/now`); console.log(s.name, now.wind_kt, "kt", now.dir_deg + "°"); }
}, 60e3);
# pip install requests
import os, time, signal, sys, requests
BASE = "https://windwave.com.br/api/v1"
KEY = os.environ["WINDWAVE_KEY"] # ww_live_…
s = requests.Session(); token = None; renew_at = 0
def open_():
global token, renew_at
r = s.post(BASE + "/session", headers={"Authorization": "Bearer " + KEY}); j = r.json()
if not r.ok: raise RuntimeError(j["code"])
token, renew_at = j["token"], time.time() + 600
def refresh():
global token, renew_at
r = s.post(BASE + "/session/refresh", headers={"Authorization": "Bearer " + token})
if not r.ok: return open_()
token, renew_at = r.json()["token"], time.time() + 600
def api(path, method="GET"):
if not token: open_()
elif time.time() > renew_at: refresh()
r = s.request(method, BASE + path, headers={"Authorization": "Bearer " + token})
if r.status_code == 401: open_(); r = s.request(method, BASE + path, headers={"Authorization": "Bearer " + token})
if r.status_code == 429: time.sleep(int(r.headers.get("Retry-After", 60))); return api(path, method)
j = r.json()
if not r.ok: raise RuntimeError(j["code"])
return j
def close(*_):
if token: s.delete(BASE + "/session", headers={"Authorization": "Bearer " + token})
sys.exit(0)
signal.signal(signal.SIGINT, close); signal.signal(signal.SIGTERM, close)
stations = api("/stations")["data"]
while True:
for st in stations:
now = api(f"/stations/{st['id']}/now"); print(st["name"], now.get("wind_kt"), "kt")
time.sleep(60)
Segurança
- A chave fica só no servidor. Se ela aparecer em código público, num app ou numa página, peça a troca na hora.
- A Windwave não guarda a sua chave, só uma impressão digital dela. Perdeu a chave? Pedimos outra; não dá para recuperar a antiga.
- Na troca de chave, a antiga pode continuar valendo por alguns dias para você atualizar sem parar.
- Use sempre HTTPS.
- Se quiser, a chave pode aceitar só IPs ou faixas que você indicar.
Versões
O v1 no endereço só muda se algo deixar de ser compatível. Campos novos podem aparecer nas respostas a qualquer momento — ignore os que você não conhece. A versão exata vem no cabeçalho X-Windwave-Api-Version.
| Versão | Data | Mudanças |
|---|---|---|
| 1.0.0 | nov/2026 | Primeira versão: sessão, estações, histórico, câmeras e vídeo ao vivo. |
Especificação OpenAPI 3.1: /developers/openapi.json (importe no Postman, Insomnia ou gere um cliente).
Windwave API