Abre uma sessão de VPN até a IPMI
POST /cloud/servers/{id}/ipmi-vpn
Escopo do token: servers:console. Recurso: VPN da IPMI.
Com o servidor em repouso (ativo, sem sistema ou falho) e na região com VPN (422 cloud.vpn_unavailable); indisponível enquanto uma operação nossa roda (deploy, reinstalação, limpeza, exclusão) e enquanto o servidor está desligado por falta de pagamento (409 cloud.server_suspended; o desligamento encerra as sessões abertas antes de desligar). public_key é a chave pública WireGuard gerada no dispositivo do cliente: a chave privada nunca chega à plataforma. A sessão nasce opening com o endereço do túnel e a conta da IPMI (usuário e senha gerados, papel Operator); o job cloud-ipmi-vpn cria a conta na IPMI e o peer no OPNsense (active, com a chave pública do OPNsense e o arquivo do WireGuard) e tira tudo ao vencer. Uma sessão viva por servidor (409 cloud.vpn_session_open) e 5 por conta (422 cloud.quota_exceeded). Por token de API só com o escopo servers:console, e pelo servidor MCP só depois da aprovação da pessoa no portal; a senha da IPMI nunca volta ao MCP (a pessoa a lê no portal).
Exemplo de requisição
Seção intitulada “Exemplo de requisição”Os exemplos leem o token da variável de ambiente HALVER_API_TOKEN (um token sc_live_… criado em Desenvolvedores › Tokens de API). Nunca escreva o token no código.
curl -X POST "https://dash.halver.com.br/api/v1/cloud/servers/307d7f63-0f00-4d4b-bc28-839d359e13a3/ipmi-vpn" \ -H "Authorization: Bearer $HALVER_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "duration_minutes": 60, "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExemploDeChavePublicaNaoUseEm0123456789 ana@notebook"}'package main
import ( "fmt" "io" "net/http" "os" "strings" "time")
func main() { body := strings.NewReader(`{ "duration_minutes": 60, "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExemploDeChavePublicaNaoUseEm0123456789 ana@notebook"}`) req, err := http.NewRequest(http.MethodPost, "https://dash.halver.com.br/api/v1/cloud/servers/307d7f63-0f00-4d4b-bc28-839d359e13a3/ipmi-vpn", body) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("HALVER_API_TOKEN")) req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() out, err := io.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println(resp.Status) fmt.Println(string(out))}import os
import requests
resp = requests.post( "https://dash.halver.com.br/api/v1/cloud/servers/307d7f63-0f00-4d4b-bc28-839d359e13a3/ipmi-vpn", headers={ "Authorization": f"Bearer {os.environ['HALVER_API_TOKEN']}", }, json={ "duration_minutes": 60, "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExemploDeChavePublicaNaoUseEm0123456789 ana@notebook", }, timeout=30,)resp.raise_for_status()print(resp.json())Parâmetros
Seção intitulada “Parâmetros”Parâmetros do caminho
Seção intitulada “Parâmetros do caminho”-
idstring (uuid) obrigatórioExemplo:
307d7f63-0f00-4d4b-bc28-839d359e13a3.
Corpo da requisição
Seção intitulada “Corpo da requisição”application/json, obrigatório.
-
duration_minutesinteger obrigatórioUm de:
60,240,480. -
public_keystring obrigatórioChave pública WireGuard do cliente (base64 de 32 bytes, como a saída de
wg pubkey).
{ "duration_minutes": 60, "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExemploDeChavePublicaNaoUseEm0123456789 ana@notebook"}Respostas
Seção intitulada “Respostas”Sessão abrindo.
Campos da resposta
-
activated_atstring (date-time) obrigatório pode ser nulo -
apply_errorstring obrigatórioÚltima resposta da IPMI ou do OPNsense a uma mudança que não passou (“” quando não há).
-
bmcobject obrigatório-
addressstring obrigatórioIP da IPMI, alcançável só pelo túnel.
-
passwordstring obrigatórioSó enquanto a sessão está viva e só para o cliente que opera os servidores (responsável); “” nos outros casos, inclusive para o usuário de consulta.
-
urlstring obrigatório -
usernamestring obrigatórioConta da IPMI criada para a sessão (papel Operator).
-
-
close_reasonstring obrigatórioPor que encerrou (ou está encerrando). operation = uma operação nossa (deploy, reinstalação, limpeza ou exclusão) começou no servidor; suspended = o servidor foi desligado por falta de pagamento. Um de:
""(vazio),ended,staff,expired,server_gone,failed,operation,suspended. -
closed_atstring (date-time) obrigatório pode ser nulo -
created_atstring (date-time) obrigatório -
duration_minutesinteger obrigatório -
expires_atstring (date-time) obrigatório -
hostnamestring obrigatório -
idstring (uuid) obrigatório -
server_idstring (uuid) obrigatório -
statusstring obrigatórioopening = conta da IPMI e peer sendo criados; closing = peer, regras e conta sendo removidos. Um de:
opening,active,closing,closed. -
wireguardobject obrigatório-
addressstring obrigatórioEndereço do cliente no túnel (/32).
-
allowed_ipsstring obrigatórioSó a IPMI do servidor (/32).
-
configstring obrigatórioArquivo do wg-quick com
<SUA_CHAVE_PRIVADA>no lugar da chave privada (“” até ativa). -
endpointstring obrigatóriohost:porta do WireGuard do OPNsense.
-
keepaliveinteger obrigatório -
public_keystring obrigatórioChave pública do cliente.
-
server_public_keystring obrigatórioChave pública do OPNsense (“” até a sessão ficar ativa).
-
{ "activated_at": null, "apply_error": "", "bmc": { "address": "198.51.100.20", "password": "", "url": "", "username": "sc-7f3a" }, "close_reason": "ended", "closed_at": null, "created_at": "2026-10-20T18:00:00Z", "duration_minutes": 60, "expires_at": "2026-10-20T18:00:00Z", "hostname": "web-01", "id": "f6ba3404-eb7b-4ebc-8d6b-7483c6c094b0", "server_id": "307d7f63-0f00-4d4b-bc28-839d359e13a3", "status": "opening", "wireguard": { "address": "198.51.100.20", "allowed_ips": "10.250.0.0/24", "config": "", "endpoint": "198.51.100.1:51820", "keepalive": 25, "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExemploDeChavePublicaNaoUseEm0123456789 ana@notebook", "server_public_key": "" }}Todo erro vem como application/problem+json (RFC 9457), com um code estável, a mensagem em português em detail, o motivo de cada campo em errors (nos erros de validação) e o request_id, que você informa ao suporte. Veja a lista completa de códigos.
| Status | Quando | Códigos |
|---|---|---|
| 400 | Requisição inválida (JSON malformado, campo desconhecido, parâmetro inválido ou falha de validação). | validation_failed, request.invalid_json |
| 401 | Não autenticado (token ausente, inválido ou expirado; credenciais inválidas). | auth.unauthorized |
| 403 | Autenticado, mas sem permissão para a área ou a operação. | api_token.insufficient_scope |
| 404 | Recurso não encontrado (ou fora do escopo do usuário). | server.not_found |
| 409 | Conflito (unicidade ou operação concorrente em andamento). | cloud.server_suspended, cloud.vpn_session_open |
| 422 | Entrada válida, mas viola uma regra de negócio. | cloud.vpn_unavailable, cloud.quota_exceeded |
{ "type": "about:blank", "title": "Forbidden", "status": 403, "detail": "o token não tem o escopo servers:console; crie um token com esse escopo em Desenvolvedores › Tokens de API", "code": "api_token.insufficient_scope", "request_id": "6f1c0b2a9e4d7c3b5a8f0e1d"}