17.X Proxy Pool (Bright Data ISP 1:1) — Superadmin#
Cada instância WhatsApp roda atrás de um IP residencial dedicado (Bright Data, zona isp_br) — 1 IP por instância, sem compartilhamento entre clientes. Endpoints abaixo expoem o pool; clientes normais não veem.
Quando STEALTH_IP_COHERENCE_ENABLED=true, o worker reavalia o proxy no instance:connect: deriva DDD -> city pelo catalogo compartilhado de fingerprint, reaproveita um IP available saudável (last_health_ok=true) na cidade correspondente ou provisiona 1 novo IP no Bright Data nessa cidade. DDD desconhecido ou telefone ausente cai para br-saopaulo. O script noturno scripts/proxy-pool-warm-cities.sh preaquece 5 IPs livres nas 10 maiores cidades BR (br-saopaulo, br-rio-de-janeiro, br-belo-horizonte, br-curitiba, br-porto-alegre, br-brasilia, br-salvador, br-recife, br-fortaleza, br-belem) sem ultrapassar BRIGHT_DATA_MAX_POOL_SIZE.
GET/v1/admin/proxy-providers#
Lista os provedores de proxy registrados com status enabled. Usado pela
UI admin para popular o seletor de provedor no formulario de provisionamento.
Resposta sanitizada — nunca retorna API keys, base URLs ou outra configuração
sensível.
Auth: Superadmin
Resposta 200:
{
"data": [
{ "name": "bright-data", "display_name": "Bright Data", "enabled": true },
{ "name": "proxy-seller", "display_name": "Proxy-Seller", "enabled": false }
],
"total": 2
}
Erros: 503 PROXY_POOL_DISABLED — pool não configurado.
GET/v1/admin/proxy-pool#
Lista IPs do pool com totais por status e custo mensal.
Auth: Superadmin
Query: status (available|assigned|banned|cooldown|decommissioned), page, limit
Resposta 200:
{
"data": [
{
"id": 1,
"provider": "bright-data",
"zone": "isp_br",
"ip_address": "200.160.45.35",
"city": "br-saopaulo",
"isp": "",
"host": "brd.superproxy.io",
"port": 33335,
"status": "assigned",
"assigned_company_id": 3,
"assigned_instance": "9b1f8d20-312c-41fd-88d5-0de4648feefc",
"assigned_at": "2026-04-17T00:15:00Z",
"last_health_check": "2026-04-17T00:20:12Z",
"last_health_ok": true,
"last_health_error": "",
"ban_count": 0,
"ban_reason": "",
"cooldown_until": null,
"cost_usd_per_month": 4,
"expires_at": null,
"auto_renew": false,
"provider_ip_id": "",
"health_score": 100,
"health_score_updated_at": "2026-04-29T00:00:00Z",
"last_event_at": "2026-04-29T00:00:00Z",
"total_allocations": 1,
"total_pairs": 1,
"total_qr_cycles": 1,
"total_qr_timeouts": 0,
"total_connect_failures": 0,
"total_cooldowns": 0,
"last_wa_probe_at": "2026-05-04T00:00:00Z",
"last_wa_probe_ok": true,
"last_wa_probe_signal": "qr_emitted",
"last_wa_probe_latency_ms": 9420,
"reputation_score": 100,
"reputation_status": "ok",
"reputation_checked_at": "2026-05-04T00:00:00Z",
"reputation_provider_count": 2,
"reputation_summary": "",
"created_at": "2026-04-16T23:23:08Z",
"updated_at": "2026-04-16T23:23:19Z"
}
],
"total": 1,
"page": 1,
"limit": 50,
"counts_by_status": { "assigned": 1 },
"counts_by_provider": { "bright-data": { "assigned": 1 } },
"monthly_cost_usd": 4,
"monthly_cost_by_provider": { "bright-data": 4 },
"providers": [
{ "name": "bright-data", "display_name": "Bright Data", "enabled": true }
],
"zone": "isp_br",
"min_free_target": 2,
"max_pool_size": 50
}
Nota de seguranca: username e password NUNCA são retornados. Apenas host/porta públicos.
Erro comum a todos os endpoints de 17.X: 503 PROXY_POOL_DISABLED quando BRIGHT_DATA_* não está configurado.
GET/v1/admin/proxy-pool/{proxyId}#
Detalhe de um único IP. Mesmo shape do item em data acima.
POST/v1/admin/proxy-pool/sync#
Reconcilia o pool local com a zone isp_br no Bright Data. Insere IPs novos, marca decommissioned os que sumiram no provedor, não mexe em assigned.
Resposta 200:
{
"inserted": ["200.160.45.35"],
"decommissioned": null,
"unchanged": 0,
"total": 1
}
Erro: 502 BAD_GATEWAY quando a sincronização com o provedor falha.
POST/v1/admin/proxy-pool/provision#
Aloca N novos IPs dedicados no provedor escolhido. bright-data (~$4/IP/mes)
roteia pela EnsureCapacity (alocação city-aware, teto BRIGHT_DATA_MAX_POOL_SIZE,
probe BR pos-provisionamento). Outros provedores roteiam pela ProvisionForProvider.
Body:
{ "provider": "bright-data", "count": 2, "country": "BRA" }
provider(opcional, defaultbright-data) — id do provedor registrado.count(obrigatório) — deve ser >= 1.country(opcional) — código de país para provedores multi-país.
Resposta 200: { "requested": 2, "provisioned": 2, "provider": "bright-data" }
Erros:
400 BAD_REQUEST—countausente ou < 1400 PROVIDER_NOT_REGISTERED— provider informado não está registrado/habilitado502 BAD_GATEWAY— provisionamento no provedor falhou
DELETE/v1/admin/proxy-pool/{proxyId}#
Remove o IP permanentemente (no provedor E no pool local). Resposta 200: { "decommissioned": true, "id": 1 }. Erros: 409 CONFLICT (IP em assigned — libere a instância primeiro), 404 NOT_FOUND, 400 BAD_REQUEST (id inválido), 503 PROXY_POOL_DISABLED.
POST/v1/admin/proxy-pool/{proxyId}/replace#
Troca o IP de uma linha SEM encolher o pool — a ação recomendada para
"remover este IP ruim". bright-data: descomissiona a linha + provisiona um
IP BR novo (cobranca proporcional). proxy-seller: submete /proxy/replace
(gratis dentro do periodo pago). Retorna a linha (possivelmente nova) após a
substituição.
Auth: Superadmin
Resposta 200:
{ "replaced": true, "id": 7, "row": { "...": "proxyIPDTO da nova linha" } }
Erros:
400 BAD_REQUEST—proxyIdinválido404 NOT_FOUND— IP não existe429 PROXY_REPLACE_COOLDOWN— provedor está rate-limitando o replace409 CONFLICT— outra falha (ex.: IP em estado inválido)503 PROXY_POOL_DISABLED
PATCH/v1/admin/proxy-pool/{proxyId}/auto-renew#
Liga/desliga o flag auto_renew de uma linha. Aplica-se a linhas Proxy-Seller
(pre-pagas); Bright Data tem cobranca continua e auto-renovação e N/A.
Auth: Superadmin
Body: { "enabled": true }
Resposta 200: { "id": 7, "auto_renew": true }
Erros: 400 BAD_REQUEST (id inválido ou JSON malformado), 404 NOT_FOUND,
503 PROXY_POOL_DISABLED.
POST/v1/admin/proxy-pool/{proxyId}/health#
Roda um health check ao vivo: conecta via HTTP CONNECT ao ipinfo.io e confere que o exit IP bate com o esperado.
Resposta 200:
{
"id": 1,
"ok": true,
"ip_address": "200.160.45.35",
"last_health_check": "2026-04-17T00:20:12Z"
}
Erros: 503 PROXY_POOL_DISABLED, 400 BAD_REQUEST (id inválido). O body 200 também inclui last_health_error e, em caso de falha, error.
GET/v1/admin/proxy-pool/{proxyId}/health#
Snapshot de saúde atual do IP — score + histograma por tipo de evento +
timestamps de atividade. Renderiza o card "saúde do IP" ao lado da gaveta de
timeline. (Distinto do POST /v1/admin/proxy-pool/{proxyId}/health, que
dispara um health check ao vivo.)
Auth: Superadmin
Resposta 200:
{
"id": 7,
"ip_address": "200.160.45.35",
"status": "assigned",
"city": "br-saopaulo",
"provider": "bright-data",
"health_score": 100,
"health_score_updated_at": "2026-04-29T12:00:00.000Z",
"last_event_at": "2026-04-29T12:00:00.000Z",
"totals": {
"allocations": 1, "pairs": 1, "qr_cycles": 1,
"qr_timeouts": 0, "connect_failures": 0, "cooldowns": 0, "bans": 0
},
"assigned_instance": "9b1f8d20-...",
"events_by_type": { "wa_probe_ok": 1 }
}
Erros: 503 PROXY_POOL_DISABLED, 400 INVALID_ID, 404 PROXY_NOT_FOUND.
POST/v1/admin/proxy-pool/{proxyId}/pong#
Dispara uma sonda pong sob demanda através do proxy do IP até
/v1/internal/proxy-pong e persiste a evidência em proxy_health_events.
Usa exatamente o mesmo código do loop agendado de 5 minutos — um resultado
verde aqui e a mesma prova que o loop agendado produziria.
Auth: Superadmin
Resposta 200:
{
"id": 42,
"proxy_ip_id": 7,
"ip_address": "200.160.45.35",
"trigger": "manual",
"ok": true,
"match": true,
"expected_ip": "200.160.45.35",
"observed_ip": "200.160.45.35",
"rtt_ms": 312,
"x_forwarded_for": "200.160.45.35",
"user_agent": "biazap-proxy-pong/1",
"received_at": "2026-04-18T11:55:00Z",
"server_time": "2026-04-18T11:55:00Z",
"created_at": "2026-04-18T11:55:00Z"
}
Erros:
503 PROXY_POOL_DISABLED— pool não configurado503 PROXY_HEALTH_DISABLED—PROXY_HEALTH_SECRETouPROXY_HEALTH_PUBLIC_URLausente400 BAD_REQUEST—proxyIdinválido404 NOT_FOUND— IP não existe502 BAD_GATEWAY— a sonda falhou
GET/v1/admin/proxy-pool/{proxyId}/pong-history#
Retorna os últimos N eventos pong de um IP, mais recentes primeiro. Usado pela gaveta de histórico de saúde na UI admin.
Auth: Superadmin
Query: limit (default 100, max 500)
Resposta 200: { "data": [ { ...proxyHealthEventDTO... } ], "total": N, "limit": 100 }
Erros: 503 PROXY_POOL_DISABLED, 400 BAD_REQUEST (id inválido), 500 INTERNAL.
GET/v1/admin/proxy-pool/{proxyId}/events#
Log de eventos eterno de um IP do pool, mais recentes primeiro. Usado pela gaveta de timeline na UI admin. Eventos permanecem mesmo após o IP ser descomissionado.
Auth: Superadmin
Query: limit (default 200, max 1000)
Resposta 200:
{
"events": [
{
"id": 901,
"proxy_ip_id": 7,
"ip_address": "200.160.45.35",
"event_type": "wa_probe_ok",
"severity": "info",
"company_id": 3,
"instance_id": "9b1f8d20-...",
"message": "WA probe verdict=ok",
"health_delta": 5,
"data": {},
"occurred_at": "2026-04-29T12:00:00.000Z"
}
],
"total": 1,
"by_type": { "wa_probe_ok": 1 }
}
Quando o recorder está desabilitado responde
{ "events": [], "total": 0, "by_type": {}, "recorder": "disabled", "proxy_id": 7, "event_kind": "proxy" }.
Erros: 503 PROXY_POOL_DISABLED, 400 INVALID_ID, 500 INTERNAL.
POST/v1/admin/proxy-pool/{proxyId}/wa-probe#
Roda um WhatsApp QR Probe sincrono através do IP. Abre uma sessão
disposable (whatsmeow + tmpdir SQLite + SetProxyAddress), espera o
primeiro QR code (sem parear), e fecha. Detecta IPs queimados antes
de qualquer cliente ver: ConnectFailure 401/402/403/406,
StreamError 401/403, TemporaryBan -> verdict=burned. Latencia
típica 10-15s.
Pre-requisito: WORKER_RPC_URL apontando para o worker e
WA_PROBE_AT_PROVISION em enabled ou manual_only no env. IP precisa
estar status=available (proibido tocar IPs já atribuidos a um cliente
real). Verdict + signal + latency são persistidos em proxy_ips.last_wa_probe_*
e emitem um row no proxy_ip_events (eternal log).
Resposta 200:
{
"verdict": "ok",
"signal": "qr_emitted",
"latency_ms": 9420,
"ip_address": "200.160.45.35",
"provider": "bright-data"
}
Verdicts:
ok— Meta aceitou ClientPayload e emitiu QR. IP fica no pool.burned— Meta rejeitou (ban-y signal). Operador pode descomissionar via DELETE.inconclusive— timeout / ClientOutdated / 5xx. IP fica como está; tentar de novo.
Erros:
- 503
WA_PROBE_DISABLED— RPC não wired (faltouWORKER_RPC_URL). - 503
PROXY_POOL_DISABLED— Bright Data não configurado. - 502
WA_PROBE_FAILED— RPC falhou (worker down, network). - 400
BAD_REQUEST—proxyIdinválido.
Pre-release gate (provision + sync) chama o mesmo caminho via
SetValidateMetaReachabilityFunc no cmd/biazap-api/main.go —
quando WA_PROBE_AT_PROVISION=enabled, todo IP novo passa por essa
verificação antes de entrar como available. Burned verdicts no gate
disparam Decommission automático no provider; verdicts inconclusive
deixam o IP entrar com last_wa_probe_ok=NULL (re-probe manual depois).
POST/v1/admin/proxy-pool/{proxyId}/cooldown/release#
Forca um IP em cooldown de volta para available. Usado quando você está confiante de que o ban foi transiente.
Resposta 200: { "released": true, "id": 1 }
Erros: 503 PROXY_POOL_DISABLED, 400 BAD_REQUEST (id inválido), 409 CONFLICT (IP não está em cooldown).
Ciclo de vida do IP#
available --> assigned --> (banido ou desatribuido)
^ |
| v
+-- cooldown --- (72h)
|
+-- decommissioned (removido para sempre)
available: livre, pode ser alocado na próxima criação de instânciaassigned: em uso 1:1 com uma instânciabanned: reservado para casos manuaiscooldown: 72h fora do pool; auto-retorna aavailabledecommissioned: removido no provedor, mantido no DB para auditoria
Nota operacional: o loop de health pong hoje persiste evidência (
proxy_health_events,last_health_*) e incrementa metricas, mas não move automaticamente o IP paracooldownquando detecta mismatch. O cooldown continua sendo uma ação administrativa/manual.
Metricas Prometheus#
| Metrica | Labels | Descricao |
|---|---|---|
biazap_proxy_pool_total |
status |
Gauge: contagem por status |
biazap_proxy_pool_size |
city |
Gauge: tamanho atual do pool por cidade (exclui decommissioned) |
biazap_proxy_pool_cost_usd_monthly |
— | Gauge: custo mensal em USD (soma cost_usd_per_month de IPs não-decommissioned) |
biazap_proxy_alloc_total |
result (success/pool_exhausted/bd_failure) |
Counter de tentativas de alocação |
biazap_proxy_provision_total |
result (success/failure/capped/bad_ip/exhausted_retries) |
Counter de provisionamentos via BD API. bad_ip = IP provisionado mas rejeitado pela validação BR (discard no BD imediato). exhausted_retries = 5 tentativas consecutivas retornaram IPs não-BR, provisionamento abortado (operator precisa investigar). |
biazap_proxy_health_failures_total |
— | Counter de falhas em health check |
biazap_proxy_bans_total |
— | Counter de bans manuais (MarkBanned) |
biazap_fingerprint_city_total |
city |
Counter: cidade efetivamente entregue por AllocateForPhone / rebind coerente por DDD |
Env vars relacionadas#
BRIGHT_DATA_API_KEY,BRIGHT_DATA_CUSTOMER_ID,BRIGHT_DATA_ZONE,BRIGHT_DATA_ZONE_PASSWORD— credenciais provedorBRIGHT_DATA_HOST(defaultbrd.superproxy.io),BRIGHT_DATA_PORT(default 33335)BRIGHT_DATA_DEFAULT_CITY(defaultbr-saopaulo)BRIGHT_DATA_MIN_FREE_POOL(default 2) — auto top-up mantem pelo menos esses IPs livresBRIGHT_DATA_MAX_POOL_SIZE(default 100) — teto duro de IPs para controlar custoPROXY_FAIL_CLOSED(defaulttrue) — recusainstance:connect/verify quando a instância não tem binding de proxy dedicado, em vez de cair para egress direto (preserva o invariante 1:1 proxy/VPN)DNS_HARDENED(defaulttrue) — resolve as URLs de mídia/webhook via DNS-over-TLS (1.1.1.1:853) em vez do resolver do SO; defesa de SSRF apenas (cobreValidateMediaURL+ validação de URL de webhook, NÃO o tráfego de sessão whatsmeow)STEALTH_UTLS_NEW_INSTANCES_PROFILE(default vazio) — perfil uTLS sticky aplicado na criação da instância e imutavel pelo resto da vida dela; vazio aciona o seletor do pool quandoSTEALTH_UTLS_POOL_ENABLED=trueSTEALTH_UTLS_POOL_ENABLED(defaultfalse) — habilita a seleção ponderada de perfil uTLS a partir do poolfingerprint_utls_profilespara instâncias novasSTEALTH_UTLS_BURN_CHECK_ENABLED(defaultfalse) — habilita o detector horario (dark-mode) de burn de perfis uTLS
As flags de rollout
STEALTH_PROFILE_ENABLED,STEALTH_UTLS_ENABLED,STEALTH_FAIL_CLOSED,STEALTH_BEHAVIOR_ENABLED,STEALTH_HYGIENE_AUTO_ENABLED,STEALTH_IP_COHERENCE_ENABLEDeSTEALTH_DNS_HARDENED_ENABLEDforam aposentadas em 2026-04-22 — substituidas porPROXY_FAIL_CLOSED+DNS_HARDENED(ambas ON por padrão).
Resposta da instância com proxy#
Todas as respostas de instância (GET /v1/instances, GET /v1/instances/{id}) passam a incluir o campo proxy quando o pool está ativo:
{
"id": "9b1f8d20-312c-41fd-88d5-0de4648feefc",
"name": "ProxyTest1",
"status": "CONNECTED",
"proxy": {
"ip_address": "200.160.45.35",
"city": "br-saopaulo",
"isp": "",
"assigned_at": "2026-04-17T00:15:00Z"
},
"created_at": "..."
}
Erros: quando o pool está vazio e MaxPoolSize foi atingido, POST /v1/instances retorna 503 com error_code=PROXY_POOL_EXHAUSTED.