{
  "name": "HookPulse",
  "description": "Dead-man switch for webhooks/cron. Main client: AI agents — every UI action has an equivalent /api/*.",
  "build": "e65e2630",
  "base_url": "https://staging.hookpulse.net",
  "docs": {
    "llms": "https://staging.hookpulse.net/llms.txt",
    "llms_full": "https://staging.hookpulse.net/llms-full.txt",
    "openapi": "https://staging.hookpulse.net/openapi.json",
    "mcp": "https://staging.hookpulse.net/mcp",
    "pricing": "https://staging.hookpulse.net/api/pricing",
    "billing": "https://staging.hookpulse.net/api/billing",
    "human_ui": "https://staging.hookpulse.net/",
    "data_indexes": [
      {
        "id": "cep",
        "produto": "https://pontofato.com",
        "caminho": "/enderecos",
        "title": "Postal codes and addresses",
        "description": "Find addresses by location, with coordinates and a 2022 reference date. Not a current postal-code certification.",
        "hierarchy": "State → municipality → locality → street → addresses",
        "url": "https://api.pontofato.com/enderecos/",
        "formats": {
          "html": "https://api.pontofato.com/enderecos/",
          "json": "https://api.pontofato.com/enderecos/index.json",
          "md": "https://api.pontofato.com/enderecos/index.md",
          "okf": "https://api.pontofato.com/enderecos/index.okf.md"
        },
        "llms": "https://api.pontofato.com/enderecos/llms.txt",
        "openapi": "https://api.pontofato.com/enderecos/openapi.json",
        "mcp": "https://api.pontofato.com/enderecos/mcp",
        "okf": "https://api.pontofato.com/enderecos/okf/index.md",
        "access": "public-read-only",
        "pagination": {
          "max_items": 20,
          "next": "links.proximo"
        },
        "updates": "manual"
      },
      {
        "id": "editais",
        "produto": "https://editalmd.com",
        "caminho": "/licitacoes",
        "title": "Public procurement",
        "description": "Find public procurement by location and date. View documents and reading options in EditalMD.",
        "hierarchy": "Procedure → state → year → month → day → municipality → purchases",
        "url": "https://api.editalmd.com/licitacoes/",
        "formats": {
          "html": "https://api.editalmd.com/licitacoes/",
          "json": "https://api.editalmd.com/licitacoes/index.json",
          "md": "https://api.editalmd.com/licitacoes/index.md",
          "okf": "https://api.editalmd.com/licitacoes/index.okf.md"
        },
        "llms": "https://api.editalmd.com/licitacoes/llms.txt",
        "openapi": "https://api.editalmd.com/licitacoes/openapi.json",
        "mcp": "https://api.editalmd.com/licitacoes/mcp",
        "okf": "https://api.editalmd.com/licitacoes/okf/index.md",
        "access": "public-read-only",
        "pagination": {
          "max_items": 20,
          "next": "links.proximo"
        },
        "updates": "manual"
      }
    ]
  },
  "conventions": {
    "format": "JSON on `/api/*`; errors are `{ error, detail? }`.",
    "cors": "`Access-Control-Allow-Origin: *`.",
    "x402": "Paid actions return HTTP 402 with accepts[]; retry with X-PAYMENT.",
    "parity": "Touched the UI/API → apidocs + skill + MCP + llms in the same PR."
  },
  "auth": {
    "credito": "Prepaid credit token in `Authorization: Bearer cred_…` (or the `X-Credito` header). Not an account: it is a bearer of balance.",
    "none": "Public, no credential.",
    "guest": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence.",
    "session": "Account session: HttpOnly cookie `__Host-mm-auth`, set when you sign in on this site's `/conta/global` page (e-mail code or link, password or Google). Writes need the same Origin and `X-CSRF-Token` from `/api/auth/bootstrap`. No bearer token is issued to humans.",
    "token": "Operator token `METRICS_TOKEN` as Bearer or `?key=`.",
    "hook": "The monitor's own token, in `?token=` or in the `X-Hook-Token` header. Read-only: state and pings — it lets you put the monitor on a third-party dashboard without handing over the owner's credential. The owner's token is also accepted on these routes."
  },
  "endpoints": [
    {
      "method": "GET",
      "path": "/agent.json",
      "auth": "none",
      "summary": "Agent card: identity, operator, documentation, the MCP endpoint and the tools it serves. Same document as `/.well-known/agent-card.json`.",
      "grupo": "Discovery",
      "retorno": {
        "_texto": "`application/json`: `name`, `provider`, `protocol` (`mcp`), `interfaces[]` and `skills[]`."
      },
      "exemplo": "curl -s $ORIGIN/agent.json",
      "returns": "`application/json`: `name`, `provider`, `protocol` (`mcp`), `interfaces[]` and `skills[]`.",
      "url": "https://staging.hookpulse.net/agent.json",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/okf/:arquivo",
      "auth": "none",
      "summary": "OKF bundle (Open Knowledge Format v0.1): markdown with frontmatter so an agent reads the whole product without parsing HTML.",
      "grupo": "Discovery",
      "params": {
        "arquivo": {
          "desc": "`index.md`, `sobre.md`, `api.md` or `faq.md`.",
          "exemplo": "index.md"
        }
      },
      "retorno": {
        "_texto": "`text/markdown`. Start at `/okf/index.md`, which lists the bundle."
      },
      "erros": {
        "404": "File outside the bundle."
      },
      "exemplo": "curl -s $ORIGIN/okf/index.md",
      "returns": "`text/markdown`. Start at `/okf/index.md`, which lists the bundle.",
      "url": "https://staging.hookpulse.net/okf/:arquivo",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/.well-known/:arquivo",
      "auth": "none",
      "summary": "Machine discovery before the home page: `api-catalog` (RFC 9727, a linkset with the API and the MCP), `security.txt` (RFC 9116), `x402` (payment manifest: network, wallet and the routes that charge) and `mcp-registry-auth` (the official MCP registry key).",
      "grupo": "Discovery",
      "params": {
        "arquivo": {
          "desc": "`api-catalog`, `security.txt`, `x402`, `mcp-registry-auth` or `apis.json`.",
          "exemplo": "api-catalog"
        }
      },
      "retorno": {
        "_texto": "`application/linkset+json` for the api-catalog; `application/json` for x402 and apis.json; `text/plain` for the other two."
      },
      "erros": {
        "404": "Name outside the five published."
      },
      "exemplo": "curl -s $ORIGIN/.well-known/api-catalog",
      "returns": "`application/linkset+json` for the api-catalog; `application/json` for x402 and apis.json; `text/plain` for the other two.",
      "url": "https://staging.hookpulse.net/.well-known/:arquivo",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/apis.json",
      "auth": "none",
      "summary": "APIs.json (apisjson.org, 0.19): the index APIs.io harvests — the API, the MCP, OpenAPI, guide and OKF bundle in one file. Also at `/.well-known/apis.json`.",
      "grupo": "Discovery",
      "retorno": {
        "_texto": "`application/json` in the APIs.json 0.19 format: `apis[]` with `baseURL`, `humanURL` and `properties[]`."
      },
      "exemplo": "curl -s $ORIGIN/apis.json",
      "returns": "`application/json` in the APIs.json 0.19 format: `apis[]` with `baseURL`, `humanURL` and `properties[]`.",
      "url": "https://staging.hookpulse.net/apis.json",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/",
      "auth": "none",
      "summary": "Self-describing index: the whole API surface, with quota and quickstart.",
      "grupo": "Discovery",
      "retorno": {
        "name": {
          "tipo": "string",
          "desc": "Product name."
        },
        "description": {
          "tipo": "string",
          "desc": "What the product does, in one sentence."
        },
        "build": {
          "tipo": "string",
          "desc": "Deployed commit."
        },
        "base_url": {
          "tipo": "string",
          "desc": "Origin this API is serving from."
        },
        "docs": {
          "tipo": "object",
          "desc": "Links to llms.txt, llms-full.txt, openapi.json, MCP and the UI."
        },
        "conventions": {
          "tipo": "object",
          "desc": "Error format, CORS, x402 and the UI↔API parity rule."
        },
        "auth": {
          "tipo": "object",
          "desc": "Each authentication mode and how to obtain it."
        },
        "endpoints": {
          "tipo": "object[]",
          "desc": "Every endpoint with method, path, auth, absolute URL and what it returns."
        },
        "quota": {
          "tipo": "object",
          "desc": "What is free, what costs and how to pay — before you spend a call."
        },
        "mcp": {
          "tipo": "object",
          "desc": "Address and transport of the MCP server."
        },
        "mcp_tools": {
          "tipo": "string[]",
          "desc": "Name of each MCP tool."
        },
        "quickstart": {
          "tipo": "string[]",
          "desc": "The calls that take you from zero to the first monitor."
        }
      },
      "returns": "{ name, description, build, base_url, docs, conventions, auth, endpoints, quota, mcp, mcp_tools, quickstart }",
      "url": "https://staging.hookpulse.net/api/",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/health",
      "auth": "none",
      "summary": "Liveness and the commit deployed right now — it is how the smoke waits for its own deploy.",
      "grupo": "Discovery",
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Always `true` when the Worker answers."
        },
        "app": {
          "tipo": "string",
          "desc": "Product name."
        },
        "build": {
          "tipo": "string",
          "desc": "Deployed commit; CI passes the short SHA on deploy."
        },
        "ts": {
          "tipo": "string",
          "desc": "Time of the response (UTC, ISO-8601)."
        }
      },
      "returns": "{ ok, app, build, ts }",
      "url": "https://staging.hookpulse.net/api/health",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "POST",
      "path": "/mcp",
      "auth": "none",
      "summary": "MCP server over HTTP (Streamable HTTP, JSON-RPC 2.0) — plugs into the client with nothing to install.",
      "grupo": "Discovery",
      "desc": "The tools are the operations of this same catalog; the MCP has no backend of its own. `GET /mcp` returns the server card.",
      "retorno": {
        "_texto": "JSON-RPC 2.0 response (`initialize`, `tools/list` or `tools/call`)."
      },
      "notes": [
        "Credentials go in the usual headers (X-Guest-Token, Authorization, X-PAYMENT) and are forwarded to the API.",
        "An exhausted quota comes back as a 402 with accepts[] inside the tool result — pay and repeat."
      ],
      "exemplo": "curl -s -XPOST $ORIGIN/mcp -H 'content-type: application/json' -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'",
      "returns": "JSON-RPC 2.0 response (`initialize`, `tools/list` or `tools/call`).",
      "url": "https://staging.hookpulse.net/mcp",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "POST",
      "path": "/api/guest",
      "auth": "none",
      "summary": "Creates a guest `hp_…` — it is the anonymous owner of your monitors.",
      "grupo": "Identity",
      "desc": "Keep the token: without it there is no way back to the monitors, unless you have already tied them to an e-mail.",
      "retorno": {
        "token": {
          "tipo": "string",
          "desc": "The guest, prefix `hp_`. Send it in `X-Guest-Token` or as Bearer."
        }
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/guest",
      "returns": "{ token }",
      "url": "https://staging.hookpulse.net/api/guest",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/endpoints",
      "auth": "guest",
      "summary": "Lists the owner's monitors, with the state of each one.",
      "grupo": "Monitors",
      "query": {
        "tag": {
          "tipo": "string",
          "desc": "Only monitors carrying this tag. Repeat it to narrow further — `?tag=prod&tag=backup` returns the monitors that have BOTH. Matched after the same normalisation used when saving, so `PROD` finds `prod`; a tag that does not pass the rule matches nothing."
        }
      },
      "retorno": {
        "endpoints": {
          "tipo": "Monitor[]",
          "desc": "The owner's monitors, without the secret fields."
        },
        "tags": {
          "tipo": "string[]",
          "desc": "The tags you filtered by, echoed back. Absent when you did not filter.",
          "opcional": true
        },
        "guest": {
          "tipo": "string",
          "desc": "The guest that owns this list.",
          "nulo": true
        },
        "billing": {
          "tipo": "Billing",
          "desc": "Prices and allowance, to decide before creating the next one.",
          "opcional": true
        }
      },
      "erros": [
        401
      ],
      "exemplo": "curl -s \"$ORIGIN/api/endpoints?tag=prod\" -H \"X-Guest-Token: $HP\"",
      "returns": "{ endpoints[{id,name,interval_sec,cron,tz,grace_sec,next_expected_at,alert_to,alert_url,last_event_at,last_status,last_latency_ms,last_start_at,last_run_id,last_duration_ms,duration,max_duration_sec,alert_pending,miss_count,alerted_at,alert_repeat_sec,tags,paused_until,state,active,healthy,overdue,waiting_first_ping,created_at,ingest_url,token?,status_url?,events_url?,curl_example?,templates?}], tags?, guest, billing?{provider,mode,network,chain_id,pay_to,homolog,dev,dev_gate,gratis?,facilitator,asset,asset_address,faucet,wallets,product,free_max_endpoints,free_min_interval_sec,free_email_alerts,prices,usage,trial} }",
      "url": "https://staging.hookpulse.net/api/endpoints",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "POST",
      "path": "/api/import/crontab",
      "auth": "guest",
      "summary": "Paste a `crontab -l` and get one monitor per scheduled line.",
      "grupo": "Monitors",
      "desc": "The body is the crontab itself, as `text/plain`. Without `?apply=1` **nothing is written**: you get the proposal — what would become a monitor, what was ignored and why, and what still fits in the free tier. With `apply=1` it is all or nothing, under the same rules as `POST /api/endpoints`. Comments, variables and `@reboot` are ignored (there is no way to know when to expect a reboot); `@daily` and friends become the equivalent expression. The command becomes the monitor name, with passwords and tokens redacted first — the name travels in the alert e-mail, in the public dashboard and in the badge.",
      "query": {
        "apply": {
          "tipo": "string",
          "desc": "`1` creates the monitors; anything else only proposes."
        }
      },
      "corpo": {
        "crontab": {
          "tipo": "string",
          "desc": "The crontab itself, sent as the raw `text/plain` body (not JSON) — the output of `crontab -l`.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "applied": {
          "tipo": "bool",
          "desc": "`false` on a proposal, `true` when the monitors were created."
        },
        "recognized": {
          "tipo": "object[]",
          "desc": "Line, cron expression, name and command of each schedule read."
        },
        "ignored": {
          "tipo": "object[]",
          "desc": "Line, reason (`comentario`, `variavel`, `reboot`, `cron_invalido`, `nao_reconhecida`) and the text."
        },
        "existing": {
          "tipo": "object[]",
          "desc": "Lines whose monitor you already have — they are not created again."
        },
        "would_create": {
          "tipo": "int",
          "desc": "How many monitors the import would create."
        },
        "created": {
          "tipo": "object[]",
          "desc": "With `apply=1`: id, name and ingest URL of each monitor created.",
          "opcional": true
        },
        "free_tier": {
          "tipo": "object",
          "desc": "Allowance and prices, to decide before applying."
        }
      },
      "erros": {
        "400": "`crontab_sem_agenda` — no schedule line was recognised.",
        "401": null,
        "402": null
      },
      "exemplo": "crontab -l | curl -s -XPOST \"$ORIGIN/api/import/crontab?apply=1\" -H \"X-Guest-Token: $HP\" -H \"content-type: text/plain\" --data-binary @-",
      "returns": "{ applied, recognized, ignored, existing, would_create, created?, free_tier }",
      "url": "https://staging.hookpulse.net/api/import/crontab",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/api/endpoints/:id/events",
      "auth": "hook",
      "summary": "The latest pings received at this monitor's ingest.",
      "grupo": "Monitors",
      "params": {
        "id": {
          "desc": "ID of the monitor."
        }
      },
      "query": {
        "token": {
          "tipo": "string",
          "desc": "Monitor token, alternative to the `X-Hook-Token` header."
        }
      },
      "retorno": {
        "events": {
          "tipo": "Ping[]",
          "desc": "The most recent pings, newest first."
        }
      },
      "erros": [
        401,
        404
      ],
      "exemplo": "curl -s \"$ORIGIN/api/endpoints/ID/events?token=$MONITOR_TOKEN\"",
      "returns": "{ events[{kind,rid,duration_ms,exit_code,at,status,latency_ms,source}] }",
      "url": "https://staging.hookpulse.net/api/endpoints/:id/events",
      "auth_detail": "The monitor's own token, in `?token=` or in the `X-Hook-Token` header. Read-only: state and pings — it lets you put the monitor on a third-party dashboard without handing over the owner's credential. The owner's token is also accepted on these routes."
    },
    {
      "method": "POST",
      "path": "/api/endpoints",
      "auth": "guest",
      "summary": "Creates a dead-man switch: silence beyond the interval becomes an alert.",
      "grupo": "Monitors",
      "desc": "This response is the only one that shows the monitor's `token` and the `templates` — keep them. The second monitor, or an interval below the free minimum, answers **402 with `accepts[]`**: pay and repeat. A miss alerts at most once per 24h — or per `alert_repeat_sec`, or per interval, whichever is longer. Send `cron`+`tz`+`grace_sec` instead of `interval_sec` for a real schedule: a 03:00 backup is late at 03:01:30, not 24 hours later. `max_duration_sec` catches the other failure: a run that starts and hangs, which plain silence detection only notices at the next scheduled time.",
      "corpo": {
        "name": {
          "tipo": "string",
          "desc": "Name to recognise the monitor in the alert — it travels in the e-mail subject, the badge and the public dashboard. Required: we never invent one for you. Trimmed, at most 80 characters, any alphabet; punctuation alone and invisible characters are rejected.",
          "obrigatorio": true
        },
        "interval_sec": {
          "tipo": "int",
          "desc": "Tolerated silence, in seconds. Below the free minimum, it costs."
        },
        "cron": {
          "tipo": "string",
          "desc": "Five-field cron expression (`0 3 * * *`). Mutually exclusive with `interval_sec`: with a schedule, the deadline is the scheduled time plus `grace_sec`."
        },
        "tz": {
          "tipo": "string",
          "desc": "IANA time zone the `cron` is read in. Default `UTC`. Only with `cron`."
        },
        "grace_sec": {
          "tipo": "int",
          "desc": "Seconds of tolerance after the scheduled time before it counts as late. Default 90, minimum 30, maximum 86400. Under the free minimum interval it costs, same rule as `interval_sec`."
        },
        "paused_until": {
          "tipo": "string",
          "desc": "Maintenance window: while this UTC instant is in the future the monitor is not checked. At most 30 days ahead."
        },
        "max_duration_sec": {
          "tipo": "int",
          "desc": "Alerts with `reason: \"too_long\"` when a run opened by `/in/:id/start` has not finished within this many seconds. 60 to 86400; `null` or `0` turns it off. Detection happens on the next 5-minute check."
        },
        "alert_repeat_sec": {
          "tipo": "int",
          "desc": "How long before the SAME incident alerts again. Minimum 3600, maximum 2592000 (30 days, effectively \"tell me once\"); `null` keeps the default of 86400. Your `interval_sec` is the floor: nothing new happened between two checks."
        },
        "tags": {
          "tipo": "string[]",
          "desc": "Up to 8 tags to group this monitor: lower-case letters, digits, `.`, `_` and `-`, at most 32 characters each. A comma-separated string works too. We lower-case and de-duplicate, so `Prod` and `prod` are the same tag."
        },
        "alert_to": {
          "tipo": "string",
          "desc": "E-mail to alert on a miss; without it, the account is alerted."
        },
        "alert_url": {
          "tipo": "string",
          "desc": "Public HTTPS URL that receives a POST on a miss (Slack Incoming, Discord, n8n)."
        }
      },
      "body": {
        "name": "prod cron",
        "interval_sec": 900,
        "alert_to": "optional@email.com",
        "alert_url": "https://n8n.example/webhook/hp"
      },
      "retorno": "Monitor",
      "erros": {
        "400": "The name answers with a code: `nome_obrigatorio` (missing, empty or blank) or `nome_invalido` (not a string, punctuation only, or carrying control/format characters). Also an invalid interval or an `alert_url` that is not public HTTPS. The schedule answers with a code: `cron_ou_interval` (both sent), `cron_invalido`, `tz_invalida`, `grace_invalido`, `cron_sem_ocorrencia` (never happens within 366 days). The maintenance window answers `pausa_invalida` (not a future instant) or `pausa_longa` (over 30 days).",
        "401": null,
        "402": null
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/endpoints -H \"X-Guest-Token: $HP\" -H 'content-type: application/json' -d '{\"name\":\"prod cron\",\"interval_sec\":900}'",
      "returns": "{ id, name, interval_sec, cron, tz, grace_sec, next_expected_at, alert_to, alert_url, last_event_at, last_status, last_latency_ms, last_start_at, last_run_id, last_duration_ms, duration, max_duration_sec, alert_pending, miss_count, alerted_at, alert_repeat_sec, tags, paused_until, state, active, healthy, overdue, waiting_first_ping, created_at, ingest_url, token?, status_url?, events_url?, curl_example?, templates?{ingest_curl,ingest_cron,ingest_n8n,miss_json,recovery_json,miss_url_hint} }",
      "url": "https://staging.hookpulse.net/api/endpoints",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/api/endpoints/:id",
      "auth": "hook",
      "summary": "State of one monitor — accepts the owner's token or the monitor's own token.",
      "grupo": "Monitors",
      "desc": "The monitor token only reads: it lets you put the state on a third-party dashboard without handing over the owner's credential. With a schedule it is here that `next_expected_at` says when the next run is due. Only this endpoint carries `duration`: `p50_ms` (the low median, so always a duration that really happened) and `max_ms` over the last runs we measured, computed at read time over the event window we already keep — the list does not pay for it.",
      "params": {
        "id": {
          "desc": "Monitor ID, from `Monitor.id`."
        }
      },
      "query": {
        "token": {
          "tipo": "string",
          "desc": "Monitor token, alternative to the `X-Hook-Token` header."
        }
      },
      "retorno": "Monitor",
      "erros": [
        401,
        404
      ],
      "exemplo": "curl -s \"$ORIGIN/api/endpoints/ID?token=$MONITOR_TOKEN\"",
      "returns": "{ id, name, interval_sec, cron, tz, grace_sec, next_expected_at, alert_to, alert_url, last_event_at, last_status, last_latency_ms, last_start_at, last_run_id, last_duration_ms, duration, max_duration_sec, alert_pending, miss_count, alerted_at, alert_repeat_sec, tags, paused_until, state, active, healthy, overdue, waiting_first_ping, created_at, ingest_url, token?, status_url?, events_url?, curl_example?, templates?{ingest_curl,ingest_cron,ingest_n8n,miss_json,recovery_json,miss_url_hint} }",
      "url": "https://staging.hookpulse.net/api/endpoints/:id",
      "auth_detail": "The monitor's own token, in `?token=` or in the `X-Hook-Token` header. Read-only: state and pings — it lets you put the monitor on a third-party dashboard without handing over the owner's credential. The owner's token is also accepted on these routes."
    },
    {
      "method": "PATCH",
      "path": "/api/endpoints/:id",
      "auth": "guest",
      "summary": "Changes the monitor's name, interval or alert channels.",
      "grupo": "Monitors",
      "desc": "Lowering the interval below the free minimum costs: the response becomes 402 with `accepts[]` until paid. Editing anything else keeps `next_expected_at` where it was — renaming a late monitor does not make it healthy.",
      "params": {
        "id": {
          "desc": "ID of the monitor to change."
        }
      },
      "corpo": {
        "name": {
          "tipo": "string",
          "desc": "New monitor name, as it shows in the alert. Same rule as on creation — omit it to keep the current one; an empty name is rejected rather than accepted, because a monitor with no name makes its own alert useless."
        },
        "interval_sec": {
          "tipo": "int",
          "desc": "New tolerated silence, in seconds. Rejected while a `cron` is set."
        },
        "cron": {
          "tipo": "string",
          "desc": "New cron expression; `null` drops the schedule and puts the monitor back on `interval_sec`.",
          "nulo": true
        },
        "tz": {
          "tipo": "string",
          "desc": "New IANA time zone; `null` goes back to `UTC`.",
          "nulo": true
        },
        "grace_sec": {
          "tipo": "int",
          "desc": "New tolerance in seconds; `null` goes back to 90.",
          "nulo": true
        },
        "paused_until": {
          "tipo": "string",
          "desc": "Maintenance window: while this UTC instant is in the future the monitor is not checked and never alerts. `null` resumes it. At most 30 days ahead.",
          "nulo": true
        },
        "max_duration_sec": {
          "tipo": "int",
          "desc": "New ceiling for an open run, 60 to 86400 seconds; `null` or `0` turns it off.",
          "nulo": true
        },
        "alert_repeat_sec": {
          "tipo": "int",
          "desc": "New gap between repeats of the same incident, 3600 to 2592000 seconds; `null` goes back to 86400.",
          "nulo": true
        },
        "tags": {
          "tipo": "string[]",
          "desc": "New tags, replacing the old ones. `null` or `[]` removes them all.",
          "nulo": true
        },
        "alert_to": {
          "tipo": "string",
          "desc": "New alert e-mail; `null` turns it off.",
          "nulo": true
        },
        "alert_url": {
          "tipo": "string",
          "desc": "New alert URL; `null` turns it off.",
          "nulo": true
        }
      },
      "body": {
        "name": "…",
        "interval_sec": 300,
        "alert_to": null,
        "alert_url": null
      },
      "retorno": "Monitor",
      "erros": {
        "400": "Invalid field in the body, with the same codes as the creation — `nome_obrigatorio` and `nome_invalido` included.",
        "401": null,
        "402": null,
        "404": null
      },
      "exemplo": "curl -s -XPATCH $ORIGIN/api/endpoints/ID -H \"X-Guest-Token: $HP\" -H 'content-type: application/json' -d '{\"interval_sec\":1800}'",
      "returns": "{ id, name, interval_sec, cron, tz, grace_sec, next_expected_at, alert_to, alert_url, last_event_at, last_status, last_latency_ms, last_start_at, last_run_id, last_duration_ms, duration, max_duration_sec, alert_pending, miss_count, alerted_at, alert_repeat_sec, tags, paused_until, state, active, healthy, overdue, waiting_first_ping, created_at, ingest_url, token?, status_url?, events_url?, curl_example?, templates?{ingest_curl,ingest_cron,ingest_n8n,miss_json,recovery_json,miss_url_hint} }",
      "url": "https://staging.hookpulse.net/api/endpoints/:id",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "DELETE",
      "path": "/api/endpoints/:id",
      "auth": "guest",
      "summary": "Deactivates the owner's monitor; it stops taking pings and alerting.",
      "grupo": "Monitors",
      "params": {
        "id": {
          "desc": "ID of the monitor to deactivate."
        }
      },
      "retorno": "Ok",
      "erros": [
        401,
        404
      ],
      "exemplo": "curl -s -XDELETE $ORIGIN/api/endpoints/ID -H \"X-Guest-Token: $HP\"",
      "returns": "{ ok }",
      "url": "https://staging.hookpulse.net/api/endpoints/:id",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/api/ingest-key",
      "auth": "guest",
      "summary": "Returns (minting it the first time) the key that lets a ping create its own monitor.",
      "grupo": "Ingest",
      "desc": "One key per owner. Point a whole fleet at `/in/<key>/<slug>` and each job creates its monitor on the first check-in, **inside the free tier** — beyond it the ping answers 402 without creating anything. The key survives signing in: the crontab you deployed keeps working.",
      "retorno": {
        "key": {
          "tipo": "string",
          "desc": "The ingest key, prefix `hpk_`."
        },
        "rotated": {
          "tipo": "bool",
          "desc": "`true` when this response came from a rotation."
        },
        "exemplo": {
          "tipo": "string",
          "desc": "A ready URL: `<origin>/in/<key>/nightly-backup`."
        },
        "note": {
          "tipo": "string",
          "desc": "What the slug means and what rotating does."
        }
      },
      "erros": [
        401
      ],
      "exemplo": "curl -s $ORIGIN/api/ingest-key -H \"X-Guest-Token: $HP\"",
      "returns": "{ key, rotated, exemplo, note }",
      "url": "https://staging.hookpulse.net/api/ingest-key",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "DELETE",
      "path": "/api/ingest-key",
      "auth": "guest",
      "summary": "Rotates the ingest key: the previous one stops creating monitors immediately.",
      "grupo": "Ingest",
      "desc": "Monitors already created keep working — rotating only revokes the power to create new ones.",
      "retorno": {
        "key": {
          "tipo": "string",
          "desc": "The new key."
        },
        "rotated": {
          "tipo": "bool",
          "desc": "Always `true` here."
        },
        "exemplo": {
          "tipo": "string",
          "desc": "A ready URL with the new key."
        },
        "note": {
          "tipo": "string",
          "desc": "What the slug means and what rotating does."
        }
      },
      "erros": [
        401
      ],
      "exemplo": "curl -s -XDELETE $ORIGIN/api/ingest-key -H \"X-Guest-Token: $HP\"",
      "returns": "{ key, rotated, exemplo, note }",
      "url": "https://staging.hookpulse.net/api/ingest-key",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/in/:ingestKey/:slug",
      "auth": "none",
      "summary": "Ping that CREATES its own monitor on the first hit — a whole fleet with no setup.",
      "grupo": "Ingest",
      "desc": "The slug becomes the monitor name and its identity: the same slug always means the same monitor, so a retry never creates a second one. Normalised to lowercase, `[a-z0-9-]`, 40 chars. Creation happens **only inside the free tier** (or the trial); past it the ping is a 402 with `accepts[]` and nothing is written — not even the event. Works by GET or POST.",
      "params": {
        "ingestKey": {
          "desc": "Ingest key, from `GET /api/ingest-key`.",
          "valores": [
            "hpk_exemplo"
          ]
        },
        "slug": {
          "desc": "Name of the job; it becomes the monitor name."
        }
      },
      "query": {
        "interval": {
          "tipo": "int",
          "desc": "Tolerated silence of the monitor being created, in seconds."
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Always `true` when the ping was recorded."
        },
        "endpoint_id": {
          "tipo": "string",
          "desc": "The monitor's id — new or the one that already existed."
        },
        "created": {
          "tipo": "bool",
          "desc": "`true` only on the hit that created it."
        }
      },
      "erros": {
        "400": "`slug_invalido` — nothing usable left after normalising.",
        "402": "The free tier is full: pay and repeat, or delete a monitor.",
        "404": "Unknown or rotated ingest key."
      },
      "exemplo": "curl -fsS \"$ORIGIN/in/$INGEST_KEY/nightly-backup\"",
      "returns": "{ ok, endpoint_id, created }",
      "url": "https://staging.hookpulse.net/in/:ingestKey/:slug",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/in/:id",
      "auth": "none",
      "summary": "The ingest URL: this is where your cron or webhook proves it is alive.",
      "grupo": "Ingest",
      "desc": "No credential on purpose — the caller is a cron, and the ID is already the secret. It costs nothing and there is no quota per ping.",
      "params": {
        "id": {
          "desc": "Monitor ID, from `Monitor.ingest_url`."
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Always `true` when the ping was recorded."
        }
      },
      "erros": {
        "404": "The monitor does not exist or is deactivated."
      },
      "exemplo": "curl -fsS $ORIGIN/in/ID",
      "returns": "{ ok }",
      "url": "https://staging.hookpulse.net/in/:id",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/in/:id/start",
      "auth": "none",
      "summary": "Says the routine STARTED — it is what turns silence into duration.",
      "grupo": "Ingest",
      "desc": "`start` is not proof of life: it does not move `last_event_at` and does not clear the miss counter, so a routine that starts and hangs still becomes a miss at the right time. The next success closes the run and records `last_duration_ms` — pass `?rid=` on both ends when runs can overlap, or the duration is measured against the wrong start. With `max_duration_sec` set on the monitor, a run left open past that ceiling alerts with `reason: \"too_long\"` on the next 5-minute check. Works by GET or POST.",
      "params": {
        "id": {
          "desc": "Monitor ID, from `Monitor.ingest_url`."
        }
      },
      "query": {
        "rid": {
          "tipo": "string",
          "desc": "Run identifier that pairs this start with the check-in that closes it. `[A-Za-z0-9._-]`, up to 64 characters. Send it when two runs of the same routine can overlap — without it the second start replaces the first, and the duration we report belongs to the wrong run. A value that does not pass the rule is ignored, never rejected: refusing a check-in would turn a live routine into a miss."
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Always `true` when the signal was recorded."
        },
        "kind": {
          "tipo": "string",
          "desc": "The signal recorded: `start`."
        }
      },
      "erros": {
        "404": "The monitor does not exist or is deactivated."
      },
      "exemplo": "curl -fsS $ORIGIN/in/ID/start",
      "returns": "{ ok, kind }",
      "url": "https://staging.hookpulse.net/in/:id/start",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/in/:id/fail",
      "auth": "none",
      "summary": "Says the routine FAILED — the alert leaves on the next 5-minute check.",
      "grupo": "Ingest",
      "desc": "Failing is not proof of life either: the clock keeps running. The ingest never sends e-mail and never calls out — it records `alert_pending` and the cron turns it into an alert with `reason: \"fail\"` within 5 minutes, under the same one-per-24h cap. Works by GET or POST.",
      "params": {
        "id": {
          "desc": "Monitor ID, from `Monitor.ingest_url`."
        }
      },
      "query": {
        "rid": {
          "tipo": "string",
          "desc": "The `rid` of the run this closes. It only measures — and only clears the open run — when it matches the `rid` that opened it; a different one leaves the open run alone, so a concurrent run is not cut short. Sending none closes whatever is open."
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Always `true` when the signal was recorded."
        },
        "kind": {
          "tipo": "string",
          "desc": "The signal recorded: `fail`."
        }
      },
      "erros": {
        "404": "The monitor does not exist or is deactivated."
      },
      "exemplo": "curl -fsS $ORIGIN/in/ID/fail",
      "returns": "{ ok, kind }",
      "url": "https://staging.hookpulse.net/in/:id/fail",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/in/:id/:exit_code",
      "auth": "none",
      "summary": "The shell's exit status, straight from `$?`: 0 is success, anything else is a failure.",
      "grupo": "Ingest",
      "desc": "`curl -fsS $ORIGIN/in/ID/$?` at the end of a script is the whole integration. `0` behaves exactly like a plain ping; 1 to 255 record the code and leave the failure for the cron.",
      "params": {
        "id": {
          "desc": "Monitor ID, from `Monitor.ingest_url`."
        },
        "exit_code": {
          "desc": "Exit status, 0 to 255. Outside that range it is a 400."
        }
      },
      "query": {
        "rid": {
          "tipo": "string",
          "desc": "The `rid` of the run this closes. It only measures — and only clears the open run — when it matches the `rid` that opened it; a different one leaves the open run alone, so a concurrent run is not cut short. Sending none closes whatever is open."
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Always `true` when the signal was recorded."
        },
        "kind": {
          "tipo": "string",
          "desc": "`ping` for 0, `fail` for anything else."
        }
      },
      "erros": {
        "400": "`exit_code_fora_da_faixa` — the exit status must be 0 to 255.",
        "404": "The monitor does not exist or is deactivated."
      },
      "exemplo": "ID=... ; ./backup.sh ; curl -fsS \"$ORIGIN/in/$ID/$?\"",
      "returns": "{ ok, kind }",
      "url": "https://staging.hookpulse.net/in/:id/:exit_code",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "POST",
      "path": "/in/:id",
      "auth": "none",
      "summary": "The same ingest by POST, for whoever can only send webhooks.",
      "grupo": "Ingest",
      "desc": "The body is optional and records `status` and `latency_ms` along with the ping.",
      "params": {
        "id": {
          "desc": "Monitor ID, from `Monitor.ingest_url`."
        }
      },
      "corpo": {
        "status": {
          "tipo": "int",
          "desc": "Status of the job that is pinging, to show in `Ping.status`."
        },
        "latency_ms": {
          "tipo": "int",
          "desc": "How long the job took, in ms."
        }
      },
      "body": {
        "status": 200,
        "latency_ms": 431
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Always `true` when the ping was recorded."
        }
      },
      "erros": {
        "404": "The monitor does not exist or is deactivated."
      },
      "exemplo": "curl -fsS -XPOST $ORIGIN/in/ID -H 'content-type: application/json' -d '{\"status\":200,\"latency_ms\":431}'",
      "returns": "{ ok }",
      "url": "https://staging.hookpulse.net/in/:id",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/status-feed",
      "auth": "guest",
      "summary": "Returns (minting it the first time) the public status URL of all your monitors.",
      "grupo": "Public dashboard",
      "desc": "The feed token only reads status: it does not create, change or delete. Whoever has the link sees the dashboard — treat it as a secret.",
      "retorno": "Feed",
      "erros": [
        401
      ],
      "exemplo": "curl -s $ORIGIN/api/status-feed -H \"X-Guest-Token: $HP\"",
      "returns": "{ token, rotated, json, rss, note, api_index }",
      "url": "https://staging.hookpulse.net/api/status-feed",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "DELETE",
      "path": "/api/status-feed",
      "auth": "guest",
      "summary": "Rotates the feed token: the previous URL stops working immediately.",
      "grupo": "Public dashboard",
      "retorno": "Feed",
      "erros": [
        401
      ],
      "exemplo": "curl -s -XDELETE $ORIGIN/api/status-feed -H \"X-Guest-Token: $HP\"",
      "returns": "{ token, rotated, json, rss, note, api_index }",
      "url": "https://staging.hookpulse.net/api/status-feed",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/s/:token",
      "auth": "none",
      "summary": "The status dashboard as a page — the link you send to the team, no header, no JS needed.",
      "grupo": "Public dashboard",
      "desc": "`/s/:token.html` is the same page. Rendered on the server, so the content is in the body and the link previews anywhere. **`noindex` by default**: the URL is the credential — whoever has the link reads the dashboard. It carries the same data as `.json`, plus each monitor's badge.",
      "params": {
        "token": {
          "desc": "Feed token, from `Feed.token`.",
          "exemplo": "k7m2p9r4t6v8w1y3z5b7c9d1"
        }
      },
      "retorno": {
        "_texto": "`text/html` — the dashboard with the summary, one row per monitor and its badge."
      },
      "erros": {
        "404": "Unknown feed token."
      },
      "exemplo": "curl -s $ORIGIN/s/FEED_TOKEN",
      "returns": "`text/html` — the dashboard with the summary, one row per monitor and its badge.",
      "url": "https://staging.hookpulse.net/s/:token",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/s/:token.json",
      "auth": "none",
      "summary": "The status dashboard as JSON, with no header at all — the token in the path is the credential.",
      "grupo": "Public dashboard",
      "desc": "`summary.all_ok` is the field for an external monitor to check without understanding the rest. The `id` of each row is a label of this dashboard, not the ingest id — rotating the feed changes both.",
      "params": {
        "token": {
          "desc": "Feed token, from `Feed.token`, with `.json` at the end.",
          "exemplo": "k7m2p9r4t6v8w1y3z5b7c9d1"
        }
      },
      "retorno": "FeedStatus",
      "erros": {
        "404": "Unknown feed token."
      },
      "exemplo": "curl -s $ORIGIN/s/FEED_TOKEN.json",
      "returns": "{ generated_at, summary{total,ok,overdue,waiting_first_ping,inactive,all_ok}, endpoints[{badge,id,name,status,healthy,overdue,interval_sec,last_event_at,miss_count,alerts}], _links }",
      "url": "https://staging.hookpulse.net/s/:token.json",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/s/:token.rss",
      "auth": "none",
      "summary": "The same dashboard as RSS 2.0, for a feed reader, a chat bot or a status page.",
      "grupo": "Public dashboard",
      "desc": "The `guid` of each item changes when the STATE changes, not on every generation — with a fixed guid the reader would never re-notify.",
      "params": {
        "token": {
          "desc": "Feed token, from `Feed.token`, with `.rss` at the end.",
          "exemplo": "k7m2p9r4t6v8w1y3z5b7c9d1"
        }
      },
      "retorno": {
        "_texto": "`application/rss+xml` — one item per monitor, with the state in the title."
      },
      "erros": {
        "404": "Unknown feed token."
      },
      "exemplo": "curl -s $ORIGIN/s/FEED_TOKEN.rss",
      "returns": "`application/rss+xml` — one item per monitor, with the state in the title.",
      "url": "https://staging.hookpulse.net/s/:token.rss",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/s/:token/metrics",
      "auth": "none",
      "summary": "The same dashboard as a Prometheus scrape target — point your existing alerting at it.",
      "grupo": "Public dashboard",
      "desc": "Three series labelled per monitor: `hookpulse_last_ping_age_seconds` (gauge; absent for a monitor that never checked in, because a zero there would read as 'just pinged'), `hookpulse_late` (gauge, 1 past the deadline) and `hookpulse_miss_total` (counter). The `id` label is this dashboard's label, not the ingest id. Reading costs no writes, so scrape as often as you like — and the URL is the credential, so treat it like one.",
      "params": {
        "token": {
          "desc": "Feed token, from `Feed.token`, with `/metrics` at the end.",
          "exemplo": "k7m2p9r4t6v8w1y3z5b7c9d1"
        }
      },
      "retorno": {
        "_texto": "`text/plain; version=0.0.4` — the Prometheus exposition format."
      },
      "erros": {
        "404": "Unknown feed token."
      },
      "exemplo": "curl -s $ORIGIN/s/FEED_TOKEN/metrics",
      "returns": "`text/plain; version=0.0.4` — the Prometheus exposition format.",
      "url": "https://staging.hookpulse.net/s/:token/metrics",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/s/:token/:monitor.svg",
      "auth": "none",
      "summary": "A badge of one monitor, as an SVG you paste straight into a README.",
      "grupo": "Public dashboard",
      "desc": "`:monitor` is the **published** id — the one in `FeedStatus.endpoints[].id`, and the same URL comes ready in `FeedStatus.endpoints[].badge`. The ingest id is deliberately not accepted: whoever has it can ping the monitor and silence the alarm. Three colours: green `ok`, red `late`, grey for `paused`, `waiting` and `inactive`. No external font, no request to anyone else, no write on our side; cached for 60 seconds with an `ETag`.",
      "params": {
        "token": {
          "desc": "Feed token, from `Feed.token`.",
          "exemplo": "k7m2p9r4t6v8w1y3z5b7c9d1"
        },
        "monitor": {
          "desc": "Published id of the monitor, from `FeedStatus.endpoints[].id` (24 hex).",
          "exemplo": "9f3c2b1d7a4e58b0c2d4e6f8"
        }
      },
      "retorno": {
        "_texto": "`image/svg+xml` — a 20px-high badge with the monitor name and its state."
      },
      "erros": {
        "404": "Unknown feed token, or a monitor that does not belong to this feed."
      },
      "exemplo": "curl -sI $ORIGIN/s/FEED_TOKEN/PUBLISHED_ID.svg",
      "returns": "`image/svg+xml` — a 20px-high badge with the monitor name and its state.",
      "url": "https://staging.hookpulse.net/s/:token/:monitor.svg",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/templates",
      "auth": "none",
      "summary": "Ingest snippets (curl, cron, n8n) and the exact JSON we POST on a miss and on a recovery.",
      "grupo": "Public dashboard",
      "desc": "It exists so nobody guesses the alert format: `miss_json` and `recovery_json` here are the same bodies that arrive at your `alert_url`. Slack and Discord receive only the field they read.",
      "retorno": {
        "origin": {
          "tipo": "string",
          "desc": "Origin used to build the snippets."
        },
        "templates": {
          "tipo": "Templates",
          "desc": "The snippets and the alert body."
        }
      },
      "returns": "{ origin, templates{ingest_curl,ingest_cron,ingest_n8n,miss_json,recovery_json,miss_url_hint} }",
      "url": "https://staging.hookpulse.net/api/templates",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/billing",
      "auth": "none",
      "summary": "Free allowance, x402 prices in force and the state of the account's trial.",
      "grupo": "Billing",
      "desc": "Check before creating the second monitor or lowering the interval: it is the number IN FORCE, not the documentation's.",
      "retorno": "Billing",
      "exemplo": "curl -s $ORIGIN/api/billing -H \"X-Guest-Token: $HP\"",
      "returns": "{ provider, mode, network, chain_id, pay_to, homolog, dev, dev_gate, gratis?, facilitator, asset, asset_address, faucet, wallets, product, free_max_endpoints, free_min_interval_sec, free_email_alerts, prices{extra_endpoint_usd,fast_interval_usd,email_alert_usd}, usage, trial{days,active,days_left?,ends_at} }",
      "url": "https://staging.hookpulse.net/api/billing",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/me",
      "auth": "session",
      "summary": "The session's account, its monitors and the state of the trial.",
      "desc": "The account is the house account, served by the account library in this process; `user.id` is the account id itself — the owner of the monitors and devices here.",
      "grupo": "Account",
      "retorno": {
        "user": {
          "tipo": "Conta",
          "desc": "The person who owns the session."
        },
        "profile": {
          "tipo": "object",
          "desc": "Global profile: `name`, `locale`, `timeZone`, `theme`, `revision`."
        },
        "endpoints": {
          "tipo": "Monitor[]",
          "desc": "The account's monitors."
        },
        "trial": {
          "tipo": "Trial",
          "desc": "The trial: how many days, whether active and when it ends (counted from the account's first use of HookPulse)."
        }
      },
      "erros": {
        "401": "invalid_session",
        "503": "auth_unavailable"
      },
      "exemplo": "await fetch(\"$ORIGIN/api/me\", {credentials: \"same-origin\"}).then(r => r.json());",
      "exemploLinguagem": "js",
      "returns": "{ user{id,email}, profile, endpoints[{id,name,interval_sec,cron,tz,grace_sec,next_expected_at,alert_to,alert_url,last_event_at,last_status,last_latency_ms,last_start_at,last_run_id,last_duration_ms,duration,max_duration_sec,alert_pending,miss_count,alerted_at,alert_repeat_sec,tags,paused_until,state,active,healthy,overdue,waiting_first_ping,created_at,ingest_url,token?,status_url?,events_url?,curl_example?,templates?}], trial{days,active,days_left?,ends_at} }",
      "url": "https://staging.hookpulse.net/api/me",
      "auth_detail": "Account session: HttpOnly cookie `__Host-mm-auth`, set when you sign in on this site's `/conta/global` page (e-mail code or link, password or Google). Writes need the same Origin and `X-CSRF-Token` from `/api/auth/bootstrap`. No bearer token is issued to humans."
    },
    {
      "method": "GET",
      "path": "/api/auth/bootstrap",
      "auth": "none",
      "grupo": "Account",
      "summary": "Prepare the browser for global sign-in.",
      "desc": "Sets a host-only HttpOnly browser cookie. CSRF is bound to the current session. No CORS.",
      "retorno": {
        "csrf": {
          "tipo": "string",
          "desc": "X-CSRF-Token"
        },
        "context": {
          "tipo": "string",
          "desc": "Opaque view context, also in X-MM-Context; not a credential / contexto opaco da vista, não é credencial."
        }
      },
      "erros": {
        "400": "invalid_request",
        "403": "invalid_origin / invalid_csrf",
        "503": "auth_unavailable: a sessão anterior é preservada / the previous session is preserved"
      },
      "returns": "{ csrf, context }",
      "url": "https://staging.hookpulse.net/api/auth/bootstrap",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/account/profile",
      "auth": "session",
      "grupo": "Account",
      "exemplo": "await fetch(\"$ORIGIN/api/account/profile\", {credentials: \"same-origin\"}).then(r => r.json());",
      "exemploLinguagem": "js",
      "summary": "Read your global profile.",
      "desc": "Reads current preferences from the account. Edit them on your account page; products never own a separate profile.",
      "retorno": {
        "_texto": "{profile:{name,locale,timeZone,theme,revision}}"
      },
      "erros": {
        "401": "invalid_session",
        "503": "auth_unavailable"
      },
      "returns": "{profile:{name,locale,timeZone,theme,revision}}",
      "url": "https://staging.hookpulse.net/api/account/profile",
      "auth_detail": "Account session: HttpOnly cookie `__Host-mm-auth`, set when you sign in on this site's `/conta/global` page (e-mail code or link, password or Google). Writes need the same Origin and `X-CSRF-Token` from `/api/auth/bootstrap`. No bearer token is issued to humans."
    },
    {
      "method": "GET",
      "path": "/api/account/avatar",
      "auth": "session",
      "grupo": "Account",
      "exemplo": "await fetch(\"$ORIGIN/api/account/avatar\", {credentials: \"same-origin\"}).then(r => {if (!r.ok) throw new Error(\"HTTP \" + r.status); return r.blob();});",
      "exemploLinguagem": "js",
      "summary": "Read your global profile photo.",
      "desc": "Private WebP, up to 64 KiB, no cache. Change it on your account. No user ID or object URL accepted.",
      "retorno": {
        "_texto": "image/webp; Cache-Control: no-store"
      },
      "erros": {
        "401": "invalid_session",
        "404": "not_found: no photo / sem foto",
        "503": "auth_unavailable"
      },
      "returns": "image/webp; Cache-Control: no-store",
      "url": "https://staging.hookpulse.net/api/account/avatar",
      "auth_detail": "Account session: HttpOnly cookie `__Host-mm-auth`, set when you sign in on this site's `/conta/global` page (e-mail code or link, password or Google). Writes need the same Origin and `X-CSRF-Token` from `/api/auth/bootstrap`. No bearer token is issued to humans."
    },
    {
      "method": "POST",
      "path": "/api/auth/logout",
      "auth": "session",
      "grupo": "Account",
      "exemplo": "// Execute no console da página do produto / Run in the product page console.\n(async () => {\n  const origin = \"$ORIGIN\";\n  const {csrf} = await fetch(origin + \"/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(origin + \"/api/auth/logout\", {\n    method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({})\n  });\n  if (!r.ok) throw new Error(\"Auth HTTP \" + r.status);\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "summary": "Revoke this product session.",
      "desc": "Bootstrap/CSRF must belong to this browser and session. Other product sessions remain active.",
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true"
        }
      },
      "erros": {
        "400": "invalid_request",
        "403": "invalid_origin / invalid_csrf",
        "503": "auth_unavailable: a sessão anterior é preservada / the previous session is preserved"
      },
      "returns": "{ ok }",
      "url": "https://staging.hookpulse.net/api/auth/logout",
      "auth_detail": "Account session: HttpOnly cookie `__Host-mm-auth`, set when you sign in on this site's `/conta/global` page (e-mail code or link, password or Google). Writes need the same Origin and `X-CSRF-Token` from `/api/auth/bootstrap`. No bearer token is issued to humans."
    },
    {
      "method": "GET",
      "path": "/api/account/keys",
      "auth": "session",
      "grupo": "Account",
      "summary": "List your API keys in this product.",
      "desc": "Never returns the key itself: name, last 4 characters, organization, creation, last use (hourly) and whether it still works.",
      "retorno": {
        "keys": {
          "tipo": "object[]",
          "desc": "`id`, `name`, `organizationId`, `last4`, `createdAt`, `lastUsedAt`, `revokedAt`, `active` (false when revoked or stopped by a password change / ending all sessions)."
        }
      },
      "erros": {
        "401": "invalid_session",
        "503": "auth_unavailable"
      },
      "exemplo": "await fetch(\"$ORIGIN/api/account/keys\", {credentials: \"same-origin\"}).then(r => r.json());",
      "exemploLinguagem": "js",
      "returns": "{ keys }",
      "url": "https://staging.hookpulse.net/api/account/keys",
      "auth_detail": "Account session: HttpOnly cookie `__Host-mm-auth`, set when you sign in on this site's `/conta/global` page (e-mail code or link, password or Google). Writes need the same Origin and `X-CSRF-Token` from `/api/auth/bootstrap`. No bearer token is issued to humans."
    },
    {
      "method": "POST",
      "path": "/api/account/keys/create",
      "auth": "session",
      "grupo": "Account",
      "summary": "Create an API key for agents and scripts.",
      "desc": "Needs a sign-in in the last 5 minutes; an organization key also needs a second factor in the session and the owner/admin role with this product enabled. At most 10 live keys per account and product. The key (`secret`) is returned ONCE.",
      "corpo": {
        "name": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "Up to 60 characters."
        },
        "organizationId": {
          "tipo": "string",
          "nulo": true,
          "obrigatorio": true,
          "desc": "`null` for an account key."
        }
      },
      "body": {
        "name": "agent",
        "organizationId": null
      },
      "retorno": {
        "key": {
          "tipo": "object",
          "desc": "`id`, `name`, `organizationId`, `last4`, `createdAt`."
        },
        "secret": {
          "tipo": "string",
          "desc": "`mmk_…`, shown once."
        }
      },
      "erros": {
        "400": "invalid_key_name / invalid_organization",
        "401": "invalid_session / reauth_required",
        "403": "invalid_origin / invalid_csrf / organization_forbidden / organization_mfa_required",
        "409": "key_limit_reached",
        "503": "auth_unavailable"
      },
      "exemplo": "(async () => {\n  const {csrf} = await fetch(\"$ORIGIN/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(\"$ORIGIN/api/account/keys/create\", {method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({name: \"agent\", organizationId: null})});\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "returns": "{ key, secret }",
      "url": "https://staging.hookpulse.net/api/account/keys/create",
      "auth_detail": "Account session: HttpOnly cookie `__Host-mm-auth`, set when you sign in on this site's `/conta/global` page (e-mail code or link, password or Google). Writes need the same Origin and `X-CSRF-Token` from `/api/auth/bootstrap`. No bearer token is issued to humans."
    },
    {
      "method": "POST",
      "path": "/api/account/keys/revoke",
      "auth": "session",
      "grupo": "Account",
      "summary": "Revoke one of your API keys.",
      "desc": "Stops the key at once. Repeating is harmless.",
      "corpo": {
        "id": {
          "tipo": "string",
          "obrigatorio": true,
          "desc": "The key `id`."
        }
      },
      "body": {
        "id": "…"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true"
        }
      },
      "erros": {
        "400": "invalid_key_id",
        "401": "invalid_session",
        "403": "invalid_origin / invalid_csrf",
        "404": "key_not_found",
        "503": "auth_unavailable"
      },
      "exemplo": "(async () => {\n  const {csrf} = await fetch(\"$ORIGIN/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(\"$ORIGIN/api/account/keys/revoke\", {method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: JSON.stringify({id: \"…\"})});\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "returns": "{ ok }",
      "url": "https://staging.hookpulse.net/api/account/keys/revoke",
      "auth_detail": "Account session: HttpOnly cookie `__Host-mm-auth`, set when you sign in on this site's `/conta/global` page (e-mail code or link, password or Google). Writes need the same Origin and `X-CSRF-Token` from `/api/auth/bootstrap`. No bearer token is issued to humans."
    },
    {
      "method": "POST",
      "path": "/api/auth/claim",
      "auth": "session",
      "grupo": "Account",
      "summary": "Moves what the guest created to the signed-in account: monitors, devices and their measurements, the status feed and the ingest key.",
      "desc": "Bootstrap/CSRF of this browser required; the page does it right after sign-in. Only what the guest still owns moves, in one transaction, and anything that collides with what the account already has stays with the guest. What the guest bought moves too. An old unsigned token that owns nothing here is refused. Repeating it is harmless (moves zero).",
      "corpo": {
        "guest_token": {
          "tipo": "string",
          "obrigatorio": false,
          "desc": "Guest `hp_…`; omitted, the `guest` cookie of this browser."
        }
      },
      "body": {},
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Whether the guest was recognized and moved."
        },
        "claimed": {
          "tipo": "object",
          "desc": "`product.movidos` (rows moved, per table), `product.apagados` (guest duplicates dropped) and `product.direitos` (purchases moved)."
        }
      },
      "erros": {
        "400": "invalid_product_claim / invalid_body",
        "401": "invalid_session",
        "403": "invalid_origin / invalid_csrf",
        "409": "unknown_guest (em `claimed.reason` / in `claimed.reason`)",
        "503": "product_claim_pending / auth_unavailable"
      },
      "exemplo": "(async () => {\n  const {csrf} = await fetch(\"$ORIGIN/api/auth/bootstrap\").then(r => r.json());\n  const r = await fetch(\"$ORIGIN/api/auth/claim\", {method: \"POST\", credentials: \"same-origin\",\n    headers: {\"Content-Type\": \"application/json\", \"X-CSRF-Token\": csrf},\n    body: \"{}\"});\n  return r.json();\n})();",
      "exemploLinguagem": "js",
      "returns": "{ ok, claimed }",
      "url": "https://staging.hookpulse.net/api/auth/claim",
      "auth_detail": "Account session: HttpOnly cookie `__Host-mm-auth`, set when you sign in on this site's `/conta/global` page (e-mail code or link, password or Google). Writes need the same Origin and `X-CSRF-Token` from `/api/auth/bootstrap`. No bearer token is issued to humans."
    },
    {
      "method": "POST",
      "path": "/api/contact",
      "auth": "none",
      "summary": "Write to the people behind the product — free, for people and agents alike.",
      "grupo": "Contact",
      "desc": "One route for a question and for a sponsorship, partnership or ad proposal (`tipo`, with the placements of `GET /api/partners`). No captcha, no account, no payment. One message every 10 seconds per network: one that arrives sooner waits its turn and then goes out — no error. The message reaches the team by e-mail, with `email` as the reply address.",
      "corpo": {
        "name": {
          "tipo": "string",
          "desc": "What to call the person writing.",
          "obrigatorio": true
        },
        "email": {
          "tipo": "string",
          "desc": "Where to reply.",
          "obrigatorio": true
        },
        "message": {
          "tipo": "string",
          "desc": "What you want to say.",
          "obrigatorio": true
        },
        "tipo": {
          "tipo": "string",
          "desc": "Proposal: `patrocinio`, `parceria` or `anuncio`. Turns on the fields below."
        },
        "empresa": {
          "tipo": "string",
          "desc": "Who is proposing, when it is a company."
        },
        "site": {
          "tipo": "string",
          "desc": "Website of who is proposing."
        },
        "orcamento": {
          "tipo": "string",
          "desc": "`ate_100`, `100_500`, `500_2000`, `2000_mais` or `a_combinar`."
        },
        "espaco": {
          "tipo": "string[]",
          "desc": "Placement ids from `GET /api/partners`, up to 6."
        },
        "duracao": {
          "tipo": "string",
          "desc": "Exposure in days: `30`, `90` or `365`."
        },
        "pagamento": {
          "tipo": "string",
          "desc": "`usdc`, `deposito` or `a_combinar`."
        }
      },
      "body": {
        "name": "Agent",
        "email": "agent@example.com",
        "message": "hello from an agent"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Always `true` when the message was accepted."
        }
      },
      "erros": {
        "400": "Validation: `code` names the field.",
        "503": "Contact is not configured on this server."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/contact -H 'content-type: application/json' -d '{\"name\":\"Agent\",\"email\":\"agent@example.com\",\"message\":\"hello from an agent\"}'",
      "returns": "{ ok }",
      "url": "https://staging.hookpulse.net/api/contact",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "POST",
      "path": "/api/visit",
      "auth": "none",
      "summary": "Ping from the interface that increments the day's visits. Agents need not call it.",
      "grupo": "Billing",
      "desc": "Smoke does not count: `X-MM-Smoke`, User-Agent `mm-smoke` or `smoke: true` in the body come in as `counted: false`.",
      "corpo": {
        "p": {
          "tipo": "string",
          "desc": "Path of the visited page."
        },
        "smoke": {
          "tipo": "bool",
          "desc": "`true` marks the call as a test and it does not enter the count."
        }
      },
      "body": {
        "p": "/"
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "Always `true`."
        },
        "counted": {
          "tipo": "bool",
          "desc": "Whether the visit entered the day's count."
        },
        "reason": {
          "tipo": "string",
          "desc": "Why it did not count, when `counted` is `false`.",
          "opcional": true
        }
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/visit -H 'content-type: application/json' -d '{\"p\":\"/\",\"smoke\":true}'",
      "returns": "{ ok, counted, reason? }",
      "url": "https://staging.hookpulse.net/api/visit",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "POST",
      "path": "/api/erro-cliente",
      "auth": "none",
      "summary": "Browser error report, sent by the interface itself. Agents need not call it.",
      "grupo": "Operations",
      "desc": "The interface reports on its own JS errors, unhandled rejections, scripts/CSS that failed to load and CSP blocks — once per session — and the app reports handled failures through `window.mmErro.relata`. The server validates the envelope, redacts credentials, e-mails and phone numbers, merges repeats of the same failure per minute and records an operational event; nothing is written to a database. It keeps no IP, cookie, query string or full User-Agent. Always answers 204, even for an invalid report.",
      "corpo": {
        "code": {
          "tipo": "string",
          "desc": "Failure code, `UI-` + letters/digits (`UI-JS-001` global error, `UI-PROMESSA-001`, `UI-RECURSO-001`, `UI-CSP-001`, `UI-APP-001` app report).",
          "obrigatorio": true
        },
        "phase": {
          "tipo": "string",
          "desc": "Where it broke, lowercase: `global`, `promessa`, `script`, `load_list`…",
          "obrigatorio": true
        },
        "path": {
          "tipo": "string",
          "desc": "Path of the open page, without query."
        },
        "message": {
          "tipo": "string",
          "desc": "Error message, up to 2000 characters."
        },
        "stack": {
          "tipo": "string",
          "desc": "Stack trace, up to 12000 characters."
        },
        "source": {
          "tipo": "string",
          "desc": "Originating script; only its path is kept."
        },
        "line": {
          "tipo": "int",
          "desc": "Line in the originating script."
        },
        "column": {
          "tipo": "int",
          "desc": "Column in the originating script."
        },
        "visivel": {
          "tipo": "bool",
          "desc": "Whether the tab was visible when it broke."
        }
      },
      "body": {
        "code": "UI-APP-001",
        "phase": "carregar_lista",
        "path": "/",
        "message": "lista 500"
      },
      "retorno": {
        "_texto": "204 with no body, always — an invalid, repeated or over-cap report also gets 204."
      },
      "exemplo": "curl -s -XPOST $ORIGIN/api/erro-cliente -H 'content-type: application/json' -d '{\"code\":\"UI-APP-001\",\"phase\":\"carregar_lista\",\"path\":\"/\",\"message\":\"lista 500\"}'",
      "returns": "204 with no body, always — an invalid, repeated or over-cap report also gets 204.",
      "url": "https://staging.hookpulse.net/api/erro-cliente",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "POST",
      "path": "/api/pagamento/aberto",
      "auth": "none",
      "statusOk": 202,
      "summary": "The interface reports a visible payment prompt. Agents must not call this route.",
      "grupo": "Operations",
      "desc": "An empty same-origin report, sent automatically when a payment prompt becomes visible. It starts no payment, grants no access and receives no identity or credentials. It writes no database row per report. Counts events, not unique people. The private operator dashboard separates API payment requests and browser payment views per UTC day; the two counts may overlap.",
      "headers": {
        "Origin": {
          "tipo": "string",
          "desc": "The page origin, identical to this route's origin.",
          "obrigatorio": true
        },
        "Sec-Fetch-Site": {
          "tipo": "string",
          "desc": "`same-origin`, set by the browser.",
          "obrigatorio": true
        },
        "X-MM-Payment-View": {
          "tipo": "string",
          "desc": "`1`, set by the shared component.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "_texto": "202 with no body when accepted; 204 when ignored. Always no-store."
      },
      "returns": "202 with no body when accepted; 204 when ignored. Always no-store.",
      "url": "https://staging.hookpulse.net/api/pagamento/aberto",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/vitrine",
      "auth": "none",
      "grupo": "Public stats",
      "summary": "The product's public numbers: traffic, agents, usage and reliability, no money.",
      "desc": "Projection published hourly by the house collector, rounded to two significant digits; `null` is a missing measurement, never zero. 15-minute cache with ETag (`If-None-Match` → 304). There is no way to send numbers through this route: publishing belongs to the collector, with its own token.",
      "retorno": {
        "v": {
          "tipo": "int",
          "desc": "Contract version (1)."
        },
        "produto": {
          "tipo": "string",
          "desc": "Product id."
        },
        "publicado": {
          "tipo": "bool",
          "desc": "`false` before the collector's first publication; then only these five keys come."
        },
        "atualizado_em": {
          "tipo": "string",
          "desc": "When the collector published (ISO 8601).",
          "nulo": true
        },
        "stale": {
          "tipo": "bool",
          "desc": "`true` when the projection is older than 26 h."
        },
        "nome": {
          "tipo": "string",
          "desc": "Product name.",
          "opcional": true
        },
        "desde": {
          "tipo": "string",
          "desc": "First day the series covers.",
          "nulo": true,
          "opcional": true
        },
        "fuso": {
          "tipo": "string",
          "desc": "Time zone of the days (`UTC`).",
          "opcional": true
        },
        "hoje": {
          "tipo": "object",
          "desc": "Today: pages by class (human, AI, bot), API calls by class, machine-surface reads and product usage.",
          "opcional": true
        },
        "dias": {
          "tipo": "object[]",
          "desc": "Up to 31 days, oldest first: `dia`, `paginas`, `api`, `api_ia`, `maquina`, `visitantes`, `uso`.",
          "opcional": true
        },
        "janelas": {
          "tipo": "object",
          "desc": "7- and 30-day sums (`d7`, `d30`).",
          "opcional": true
        },
        "visitantes": {
          "tipo": "object",
          "desc": "Unique visitors at the edge over 7 days.",
          "opcional": true
        },
        "pessoas": {
          "tipo": "object",
          "desc": "GA4 when available: users, sessions, countries, devices and who arrived from AI.",
          "nulo": true,
          "opcional": true
        },
        "agentes": {
          "tipo": "object",
          "desc": "The AI agents and bots that read the most, 7 days.",
          "opcional": true
        },
        "superficies": {
          "tipo": "object",
          "desc": "Reads of OKF, llms, well-known, OpenAPI and MCP over 7 days.",
          "opcional": true
        },
        "mcp": {
          "tipo": "object",
          "desc": "MCP calls over 7 days.",
          "opcional": true
        },
        "uso": {
          "tipo": "object",
          "desc": "Real product usage per resource: label, today, 7 and 30 days.",
          "opcional": true
        },
        "contas": {
          "tipo": "object",
          "desc": "Users and guests.",
          "nulo": true,
          "opcional": true
        },
        "confiabilidade": {
          "tipo": "object",
          "desc": "Share of requests without 5xx over 7 days, and the live build.",
          "opcional": true
        },
        "catalogo": {
          "tipo": "object",
          "desc": "Size of the catalog, when the product has one.",
          "nulo": true,
          "opcional": true
        },
        "apoio": {
          "tipo": "object",
          "desc": "Impressions and clicks per sponsor, when any.",
          "opcional": true
        }
      },
      "exemplo": "curl -s $ORIGIN/api/vitrine",
      "returns": "{ v, produto, publicado, atualizado_em, stale, nome?, desde?, fuso?, hoje?, dias?, janelas?, visitantes?, pessoas?, agentes?, superficies?, mcp?, uso?, contas?, confiabilidade?, catalogo?, apoio? }",
      "url": "https://staging.hookpulse.net/api/vitrine",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/vitrine/operador",
      "auth": "none",
      "grupo": "Public stats",
      "summary": "The product's full document on the operator panel — operator token only.",
      "headers": {
        "Authorization": {
          "tipo": "string",
          "desc": "`Bearer <METRICS_TOKEN>` — the operator class.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "produto": {
          "tipo": "string",
          "desc": "Product id."
        },
        "atualizado_em": {
          "tipo": "string",
          "desc": "When the collector published.",
          "nulo": true
        },
        "operador": {
          "tipo": "object",
          "desc": "The collector's full document, with what the public projection leaves out.",
          "nulo": true
        }
      },
      "erros": {
        "401": "No token, wrong token or a token of another class.",
        "503": "Worker without `METRICS_TOKEN` or without the control plane."
      },
      "exemplo": "curl -s $ORIGIN/api/vitrine/operador -H \"Authorization: Bearer $METRICS_TOKEN\"",
      "returns": "{ produto, atualizado_em, operador }",
      "url": "https://staging.hookpulse.net/api/vitrine/operador",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/vitrine/painel",
      "auth": "none",
      "grupo": "Public stats",
      "summary": "The whole house panel, in the shape the gm reads — operator token only.",
      "headers": {
        "Authorization": {
          "tipo": "string",
          "desc": "`Bearer <METRICS_TOKEN>` — the operator class.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "apps": {
          "tipo": "object[]",
          "desc": "One operator document per product, ordered by id."
        },
        "updated": {
          "tipo": "string",
          "desc": "When the collector closed the round.",
          "opcional": true
        },
        "totals": {
          "tipo": "object",
          "desc": "House totals.",
          "opcional": true
        }
      },
      "erros": {
        "401": "No token, wrong token or a token of another class.",
        "503": "Worker without `METRICS_TOKEN` or without the control plane."
      },
      "exemplo": "curl -s $ORIGIN/api/vitrine/painel -H \"Authorization: Bearer $METRICS_TOKEN\"",
      "returns": "{ apps, updated?, totals? }",
      "url": "https://staging.hookpulse.net/api/vitrine/painel",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/vitrine/cursores",
      "auth": "none",
      "grupo": "Public stats",
      "summary": "The resolved-error cursor per product (`borda`, `cli`) — operator token only.",
      "headers": {
        "Authorization": {
          "tipo": "string",
          "desc": "`Bearer <METRICS_TOKEN>` — the operator class.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "_texto": "JSON: `{ [product]: { borda?: ISO, cli?: ISO } }`; empty is `{}`."
      },
      "erros": {
        "401": "No token, wrong token or a token of another class.",
        "503": "Worker without `METRICS_TOKEN` or without the control plane."
      },
      "exemplo": "curl -s $ORIGIN/api/vitrine/cursores -H \"Authorization: Bearer $METRICS_TOKEN\"",
      "returns": "JSON: `{ [product]: { borda?: ISO, cli?: ISO } }`; empty is `{}`.",
      "url": "https://staging.hookpulse.net/api/vitrine/cursores",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/partners",
      "auth": "none",
      "grupo": "Partnership",
      "summary": "Partnership, sponsorship and advertising: the product's placements with a suggested price, the public numbers next to them and how to propose.",
      "desc": "Information on request, no activation: placements from the house catalogue priced in USD per 30 days (90 and 365 days discounted), sponsors in effect, an excerpt of `/api/vitrine`, the house wallet (USDC on Base) and the contact path — bank deposit, PIX or invoice are arranged in the reply. Cached for 1 hour.",
      "retorno": {
        "status": {
          "tipo": "string",
          "desc": "`sob_consulta`: information and proposal, no activation and no charge."
        },
        "produto": {
          "tipo": "string",
          "desc": "Product name."
        },
        "idioma": {
          "tipo": "string",
          "desc": "Language of the texts (the product's)."
        },
        "titulo": {
          "tipo": "string",
          "desc": "Title of the offer."
        },
        "descricao": {
          "tipo": "string",
          "desc": "One sentence about the offer."
        },
        "publico": {
          "tipo": "string",
          "desc": "Who uses the product — the audience a sponsor reaches."
        },
        "modalidades": {
          "tipo": "object[]",
          "desc": "`{ id, nome }`: patrocinio, parceria, anuncio."
        },
        "placements": {
          "tipo": "object[]",
          "desc": "The product's placements: `id`, `nome`, `onde`, `formato`, `exclusivo`, `medicao`, `price_usd_30d` (suggested; `null` is on request), `exposure[{ dias, price_usd }]` for 30, 90 and 365 days, `disponivel`."
        },
        "house_bundle": {
          "tipo": "object",
          "desc": "The house bundle: footer and agent mention across the ten products, discounted."
        },
        "parcerias": {
          "tipo": "string[]",
          "desc": "Partnership ideas the product is open to discuss."
        },
        "current_sponsors": {
          "tipo": "object[]",
          "desc": "Sponsors in effect: `id`, `nome`, `url`, `frase`, `espacos`, `ate`."
        },
        "stats": {
          "tipo": "object",
          "desc": "Excerpt of the public numbers (`hoje`, `janelas`, `agentes`, `confiabilidade`) and the `link` to `/api/vitrine`; `publicado: false` before the first publication."
        },
        "payment": {
          "tipo": "object",
          "desc": "How to pay: `rede`, `chain_id`, `ativo`, `pay_to`, `eip681` (the house wallet, when declared), `alternativas` and the `nota` — bank deposit, PIX or invoice in the reply."
        },
        "contact": {
          "tipo": "object",
          "desc": "`email`, `form_url`, `api_url` (`POST /api/contact`, free: one message every 10 s per network), `campos` (required), `campos_proposta` (the optional proposal fields, each with its accepted values), `message_template`, `instructions`."
        },
        "politica": {
          "tipo": "object",
          "desc": "Placement label, refused sectors, prepayment, deadlines."
        },
        "_links": {
          "tipo": "object",
          "desc": "`self`, `stats`, `page` (`null` until the page exists), `contact`, `casa` (the same path on the ten products)."
        }
      },
      "exemplo": "curl -s $ORIGIN/api/partners",
      "returns": "{ status, produto, idioma, titulo, descricao, publico, modalidades, placements, house_bundle, parcerias, current_sponsors, stats, payment, contact, politica, _links }",
      "url": "https://staging.hookpulse.net/api/partners",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/metrics",
      "auth": "none",
      "summary": "Metrics of the last 7 days. With the operator token, includes payments.",
      "grupo": "Billing",
      "desc": "Without credentials it returns visits and usage; `accounts.people` counts the accounts that have used HookPulse (from the shared account records — guests are not counted). With `METRICS_TOKEN` as Bearer it adds `payments` — and only on Base mainnet, because staging numbers on a finance panel mislead.",
      "headers": {
        "Authorization": {
          "tipo": "string",
          "desc": "`Bearer <METRICS_TOKEN>` to include the finance block.",
          "obrigatorio": false
        }
      },
      "retorno": "Metricas",
      "exemplo": "curl -s $ORIGIN/api/metrics -H \"Authorization: Bearer $METRICS_TOKEN\"",
      "returns": "{ app, today, today_visits, today_contacts?, days, usage, accounts, financeiro?, payments? }",
      "url": "https://staging.hookpulse.net/api/metrics",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/coletas",
      "auth": "none",
      "summary": "Lists your measurements with commands adapted to each saved device environment.",
      "grupo": "Collections",
      "desc": "Two views of the same thing: `coletas` is one entry per measurement, `equipamentos` is one per machine with a SINGLE command that does all of its measurements and a SINGLE cron line. Use the device one unless you really want a single measurement on its own. Commands are generated only from a detected environment. Legacy collections without one keep their readings, but comandos.agora/agenda are null until a compatible environment is provided. Curl, wget and Python 3 follow the detected capabilities; Windows disk uses PowerShell. comandos.agora is a readable multiline block; run the whole block together. A separate cron line is offered only when crontab exists. Unsupported formats are never guessed.",
      "retorno": {
        "coletas": {
          "tipo": "array",
          "desc": "`{id, nome, grupo, medida, ativa, criado_em, ambiente_id, ambiente, comandos}` per measurement. `ativa` is false while the measurement is stopped. comandos includes estado, agora, agenda, fonte, cliente, agendador and the three that touch the crontab — instalar (adds the line, idempotently, keeping the machine's other jobs), conferir (says on the machine whether it is scheduled) and remover; unsupported commands are null. Up to 200."
        },
        "equipamentos": {
          "tipo": "array",
          "desc": "The same collections seen as DEVICES: one entry per saved environment, `{ambiente_id, nome, grupo, ambiente, medidas, comandos}`. Its `comandos.agora` reads and sends EVERY measurement of that machine in one block, and `comandos.agenda` is a single crontab line for all of them. `comandos.instalar` installs that line, `comandos.conferir` answers on the machine whether it is there, and `comandos.remover` takes it out — all three for the whole device at once. `comandos.comecar` is the one command to start measuring: it installs the line and sends a reading right away (CPU and disk activity twice, 2 s apart, because their value needs two readings); without a scheduler it is just `agora`. Prefer these over the per-measurement ones: nobody wants seven blocks to paste or seven cron lines per server. Collections without a saved environment are absent here — there is no command to generate for them."
        },
        "inputs": {
          "tipo": "array",
          "desc": "Your collector channels, one per device and collector: `{deviceRef, deviceName, deviceGroup, id, collector, inputKind, monitoring, updatedAt, connectedAt, connected}`. `deviceName` and `deviceGroup` are the device's current name and group here — `PATCH /api/ambientes/:id` changes them — and the group is how the dashboard splits your devices into tabs. Credentials are never returned. Up to 200."
        },
        "medidas": {
          "tipo": "object",
          "desc": "The measurements on offer: `{rotulo, desc, painel, serie}` each. `painel` lists the fields to show, in order, as `{c, r, f}` — field, label and format: `mib` (a size in MiB, shown as GB/TB), `pct`, `mbps` (megabits/s), `mbs` (megabytes/s), `seg` (seconds, shown as a duration), `num`, `int`; no `f` means text."
        },
        "perfis": {
          "tipo": "object",
          "desc": "essentials and completo; expansion is filtered by the chosen environment."
        },
        "note": {
          "tipo": "string",
          "desc": "Present only when there is no session yet."
        }
      },
      "erros": [],
      "exemplo": "curl -s $ORIGIN/api/coletas -H \"X-Guest-Token: $HP\"",
      "returns": "{ coletas, equipamentos, inputs, medidas, perfis, note }",
      "url": "https://staging.hookpulse.net/api/coletas",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "POST",
      "path": "/api/coletas",
      "auth": "none",
      "summary": "Adds compatible measurements to a previously detected device.",
      "grupo": "Collections",
      "desc": "First create /api/ambientes and submit its environment report. Reference that ambiente_id here; its saved name and group are authoritative. Same device+measurement returns the existing stream instead of duplicating it. A profile adds only compatible measurements. Up to 200 collections per owner. The signed collection id writes only its own measurement stream.",
      "corpo": {
        "nome": {
          "tipo": "string",
          "desc": "Device name, up to 60 characters.",
          "obrigatorio": true
        },
        "grupo": {
          "tipo": "string",
          "desc": "Optional group, up to 40 characters."
        },
        "ambiente_id": {
          "tipo": "string",
          "desc": "UUID of a detected environment owned by this account/guest.",
          "obrigatorio": true
        },
        "medida": {
          "tipo": "string",
          "desc": "Which measurement; defaults to `loadavg`."
        },
        "perfil": {
          "tipo": "string",
          "desc": "essentials or completo. Expands to compatible measurements and takes precedence over medida."
        }
      },
      "retorno": {
        "coleta": {
          "tipo": "object",
          "desc": "First collection, including ambiente_id, ambiente and adaptive comandos."
        },
        "coletas": {
          "tipo": "array",
          "desc": "All requested compatible collections, up to eight."
        }
      },
      "erros": [
        400,
        401,
        404,
        409,
        422,
        503
      ],
      "exemplo": "curl -s -XPOST $ORIGIN/api/coletas -H 'content-type: application/json' -d '{\"nome\":\"server\",\"ambiente_id\":\"ENVIRONMENT_UUID\",\"perfil\":\"completo\"}'",
      "returns": "{ coleta, coletas }",
      "url": "https://staging.hookpulse.net/api/coletas",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/coletas/serie",
      "auth": "guest",
      "summary": "The latest readings of EVERY device you own, in one call.",
      "grupo": "Collections",
      "desc": "The same thing our own screen asks for over the socket, mirrored as plain HTTP — you are not obliged to the request-per-device pattern we stopped doing. One call, one authorisation, one query at the ingest origin: reading 66 devices one by one hits the read capacity there and comes back mostly 429, which is why this exists. Ownership is checked once, over YOUR list, so a device that is not yours is simply absent. Each item carries `api`, the URL of that single device's series; the names, groups and commands stay in `GET /api/coletas`, which is also one call. An item that could not be read comes back with `erro` and its own `retry_after_sec` instead of an empty series — a screen must say \"I could not read now\", never let you believe the machine stopped. Readings are reused for up to 60 seconds; `poll_after_sec` is the minimum wait, and it is the tightest of the items. Do not overlap requests, and respect `Retry-After` on 429 or 503.",
      "query": {
        "n": {
          "tipo": "int",
          "desc": "How many readings per device, newest first. Default 60, maximum 500."
        }
      },
      "retorno": {
        "coletas": {
          "tipo": "array",
          "desc": "One item per device you own: `{id, medida, ativa, api}` plus either `leituras` and `poll_after_sec`, or `erro` and `retry_after_sec`. A stopped measurement (`ativa:false`) carries neither — it is not read, so it reports no silence and no failure."
        },
        "poll_after_sec": {
          "tipo": "int",
          "desc": "Minimum seconds to wait before polling again; normally 60."
        }
      },
      "erros": {
        "401": null,
        "503": "Readings are temporarily unavailable, or the ingest origin is not configured. Respect Retry-After; the last readings remain valid as historical data."
      },
      "exemplo": "curl -s $ORIGIN/api/coletas/serie?n=60 -H \"X-Guest-Token: $HP\"",
      "returns": "{ coletas, poll_after_sec }",
      "url": "https://staging.hookpulse.net/api/coletas/serie",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/api/coletas/:id/serie",
      "auth": "guest",
      "summary": "The latest readings of one device, for the screen that draws it.",
      "grupo": "Collections",
      "desc": "Authorised by YOUR session, never by the symbol in the crontab. A device that is not yours answers 404 exactly like one that does not exist — telling the two apart would confirm to a stranger that the id exists — 401 is only for sending no credential at all, which tells the caller what they already know. When the ingest origin cannot be read the answer is 503, not an empty series: a screen must say \"I could not read now\", never let you believe your machine stopped. Readings are reused for up to 60 seconds. Poll no faster than once per minute in steady use; `poll_after_sec` is the minimum wait for this response. During the first minute after registration an empty series may be checked every 5 seconds. Do not overlap requests. On 429 or 503, respect `Retry-After` and `retry_after_sec`, increase the delay after repeated failures, and keep the last successful reading. Changing n does not bypass reuse or capacity limits.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "The signed 22-character collection symbol, or a legacy UUID v4.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        }
      },
      "query": {
        "n": {
          "tipo": "int",
          "desc": "How many readings, newest first. Default 60, maximum 500."
        }
      },
      "retorno": {
        "coleta": {
          "tipo": "object",
          "desc": "The collection with its saved environment and adaptive comandos."
        },
        "leituras": {
          "tipo": "array",
          "desc": "`{t, ...fields}` per reading, newest first. Disk mount details (used/available MiB) and network interfaces (measured Mb/s, nullable reported link Mb/s) accompany the latest reading; historical rows carry scalar chart values."
        },
        "poll_after_sec": {
          "tipo": "int",
          "desc": "Minimum seconds to wait before polling again; normally 60."
        }
      },
      "erros": {
        "401": null,
        "404": null,
        "429": "Read capacity reached. Wait for Retry-After; do not retry in parallel.",
        "503": "Readings are temporarily unavailable. Respect Retry-After; the last reading remains valid as historical data."
      },
      "exemplo": "curl -s $ORIGIN/api/coletas/$ID/serie?n=60 -H \"X-Guest-Token: $HP\"",
      "returns": "{ coleta, leituras, poll_after_sec }",
      "url": "https://staging.hookpulse.net/api/coletas/:id/serie",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "PATCH",
      "path": "/api/coletas/:id",
      "auth": "guest",
      "summary": "Renames one measurement, or stops and resumes reading it.",
      "grupo": "Collections",
      "desc": "The id never changes: it is the write symbol already sitting in a crontab, and renaming on a screen must not send anyone back to the machine to edit a line. Send only what you are changing; anything omitted stays as it is. `ativa:false` takes the measurement out of the fleet read — it keeps its history and its commands, stops costing a query, and comes back with `ativa:true`. It does NOT silence the machine: the symbol is signed and stands on its own at the ingest origin, so a device keeps sending until you remove its cron line. The answer is the collection as it now stands — draw the screen from it, not from a local copy.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "The signed 22-character collection symbol, or a legacy UUID v4.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        }
      },
      "corpo": {
        "nome": {
          "tipo": "string",
          "desc": "New name, 1–60 characters."
        },
        "grupo": {
          "tipo": "string",
          "desc": "New group, up to 40 characters; empty clears it."
        },
        "ativa": {
          "tipo": "bool",
          "desc": "false stops reading this measurement; true resumes it."
        }
      },
      "retorno": {
        "coleta": {
          "tipo": "object",
          "desc": "The collection after the change, with `ativa` and its adaptive comandos."
        }
      },
      "erros": {
        "400": "nome, grupo or ativa outside the accepted shape.",
        "401": null,
        "404": null
      },
      "exemplo": "curl -s -XPATCH $ORIGIN/api/coletas/$ID -H 'content-type: application/json' -H \"X-Guest-Token: $HP\" -d '{\"ativa\":false}'",
      "returns": "{ coleta }",
      "url": "https://staging.hookpulse.net/api/coletas/:id",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "DELETE",
      "path": "/api/coletas/:id",
      "auth": "guest",
      "summary": "Removes one measurement and frees that slot on the device.",
      "grupo": "Collections",
      "desc": "What disappears is the OWNERSHIP: your list, the authority to read that series, and the unique slot that measurement holds on that environment — so you can add it again later. Readings already stored at the ingest origin stay there until their own retention. The machine keeps sending: the symbol is signed and stands on its own there, which is why the answer carries `remover`, the command that takes the line out of the crontab. Use `ativa:false` instead when you only want to stop looking.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "The signed 22-character collection symbol, or a legacy UUID v4.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true when the collection was removed."
        },
        "id": {
          "tipo": "string",
          "desc": "The collection that was removed."
        },
        "deleted": {
          "tipo": "bool",
          "desc": "Always true; the row is gone, not flagged."
        },
        "instalar": {
          "tipo": "string",
          "desc": "Command that puts the cron line on the device. Idempotent — it drops our previous lines before adding the new one — and it never rewrites a crontab it could not read. Null when no cron line was generated.",
          "nulo": true
        },
        "conferir": {
          "tipo": "string",
          "desc": "Command that answers, on the device, whether the line is scheduled there. It is the only thing that can tell a removed schedule from a machine that is down: silence here is an alert, never a reason to stop a collection. Null when no cron line was generated.",
          "nulo": true
        },
        "remover": {
          "tipo": "string",
          "desc": "Command that removes this measurement's line from the device's crontab, or null when no cron line was ever generated.",
          "nulo": true
        }
      },
      "erros": {
        "401": null,
        "404": null
      },
      "exemplo": "curl -s -XDELETE $ORIGIN/api/coletas/$ID -H \"X-Guest-Token: $HP\"",
      "returns": "{ ok, id, deleted, instalar, conferir, remover }",
      "url": "https://staging.hookpulse.net/api/coletas/:id",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/api/ambientes",
      "auth": "none",
      "grupo": "Devices",
      "summary": "Saved environments owned by the current account or guest; empty without a session.",
      "retorno": {
        "ambientes": {
          "tipo": "Ambiente[]",
          "desc": "Up to 25 saved device environments."
        }
      },
      "erros": [
        401
      ],
      "exemplo": "curl -s \"$ORIGIN/api/ambientes\" -H \"X-Guest-Token: $HP\"",
      "returns": "{ ambientes[{id,nome,grupo,criado_em,expira_em,detectado_em,estado,dados,medidas,passos,sonda}] }",
      "url": "https://staging.hookpulse.net/api/ambientes",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "POST",
      "path": "/api/ambientes",
      "auth": "none",
      "grupo": "Devices",
      "summary": "Register a device UUID before discovery and collector selection.",
      "desc": "Registers one device (maximum 25 per owner), creating a guest if needed. The UUID is the one-hour discovery capability; it is not a telemetry credential. Choose passos.posix or passos.powershell for the TARGET terminal. The checks only display allowlisted system facts and install nothing.",
      "corpo": {
        "nome": {
          "tipo": "string",
          "desc": "Device name, 1–60 characters.",
          "obrigatorio": true
        },
        "grupo": {
          "tipo": "string",
          "desc": "Optional group, up to 40 characters."
        },
        "request_id": {
          "tipo": "string",
          "desc": "Optional UUID v4 generated once per registration. Becomes the device id. Retry with the same id/name/group returns the owned environment instead of duplicating a late write; conflicting input is refused."
        }
      },
      "retorno": {
        "ambiente": {
          "tipo": "Ambiente",
          "desc": "Pending environment and its short identification steps."
        }
      },
      "erros": [
        400,
        401,
        409,
        413
      ],
      "exemplo": "curl -s \"$ORIGIN/api/ambientes\" -H \"X-Guest-Token: $HP\" -H \"content-type: application/json\" -d '{\"nome\":\"server\"}'",
      "returns": "{ ambiente{id,nome,grupo,criado_em,expira_em,detectado_em,estado,dados,medidas,passos,sonda} }",
      "url": "https://staging.hookpulse.net/api/ambientes",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/ambientes/:id",
      "auth": "guest",
      "grupo": "Devices",
      "summary": "Read the detected environment and compatible measurements.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        }
      },
      "retorno": {
        "ambiente": {
          "tipo": "Ambiente",
          "desc": "Saved facts; other owners get 404."
        }
      },
      "erros": [
        401,
        404
      ],
      "exemplo": "curl -s \"$ORIGIN/api/ambientes/$ID\" -H \"X-Guest-Token: $HP\"",
      "returns": "{ ambiente{id,nome,grupo,criado_em,expira_em,detectado_em,estado,dados,medidas,passos,sonda} }",
      "url": "https://staging.hookpulse.net/api/ambientes/:id",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "POST",
      "path": "/api/ambientes/:id/relatorio",
      "auth": "none",
      "grupo": "Devices",
      "summary": "Preview or save the allowlisted discovery report for one device.",
      "desc": "Send terminal and saidas (step id to pasted output), initially with conferir:true. Preview returns normalized dados, additional passos and pronto without writing. Omit conferir to save when complete. Only allowlisted facts are saved, never raw commands. First report wins within one hour; identical retries do not rewrite. The discovery never creates a legacy measurement, sends a reading, installs software or creates a schedule. With Accept: text/plain the receipt is only `ok`.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        }
      },
      "corpo": {
        "terminal": {
          "tipo": "string",
          "desc": "posix or powershell; required with saidas."
        },
        "saidas": {
          "tipo": "object",
          "desc": "Step id to pasted output; up to 10 entries, 4096 characters each, 8192 UTF-8 bytes total request."
        },
        "conferir": {
          "tipo": "bool",
          "desc": "true previews without saving. Omit or use false to save a complete report."
        },
        "relatorio": {
          "tipo": "string",
          "desc": "Alternative legacy key=value report. Do not combine with saidas."
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true when saved, false when previewing or more checks are needed."
        },
        "dados": {
          "tipo": "object",
          "desc": "Normalized facts in previews; omitted after saving."
        },
        "medidas": {
          "tipo": "string[]",
          "desc": "Compatible measurements in previews; omitted after saving."
        },
        "passos": {
          "tipo": "array",
          "desc": "Additional {id, titulo, comando} checks in previews; omitted after saving."
        },
        "pronto": {
          "tipo": "bool",
          "desc": "Whether all requested checks have an output (possibly empty); preview only."
        }
      },
      "erros": [
        400,
        404,
        408,
        409,
        413
      ],
      "exemplo": "curl -s \"$ORIGIN/api/ambientes/$ID/relatorio\" -H \"content-type: text/plain\" --data-binary @report.txt",
      "returns": "{ ok, dados, medidas, passos, pronto }",
      "url": "https://staging.hookpulse.net/api/ambientes/:id/relatorio",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/ambientes/:id/sonda.sh",
      "auth": "none",
      "grupo": "Devices",
      "summary": "The same identification script, served for people who prefer one short line.",
      "desc": "The same script ambiente.sonda.envia.posix runs, so `curl -fsS \"$ORIGIN/api/ambientes/$ID/sonda.sh\" | sh` and pasting the displayed heredoc run the same generated script. It is indented and commented because reading it before running it is the only way `curl | sh` is honest. Comments follow Accept-Language (English or Portuguese); the code does not change with it. It reads allowlisted system facts and posts them only to this device's report URL. It sends no measurement, downloads and installs nothing, opens no port and creates no cron or scheduler entry. Available only while the one-hour report link is open; after that the body is still valid shell and exits 1, so a pipe without `-f` gets an error, never a fragment of JSON in sh.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        }
      },
      "query": {
        "modo": {
          "tipo": "string",
          "desc": "`mostra` serves the variant that only PRINTS the report, for pasting it back by hand: it carries no sending code and no report URL. Anything else serves the sending script."
        }
      },
      "retorno": {
        "_texto": "`text/plain` — a POSIX `sh` script carrying only this device's report URL."
      },
      "erros": {
        "404": "Unknown device, or its report link was already used or has expired."
      },
      "exemplo": "curl -fsS \"$ORIGIN/api/ambientes/$ID/sonda.sh\" | sh",
      "returns": "`text/plain` — a POSIX `sh` script carrying only this device's report URL.",
      "url": "https://staging.hookpulse.net/api/ambientes/:id/sonda.sh",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "POST",
      "path": "/api/ambientes/:id/inputs",
      "auth": "guest",
      "grupo": "Devices",
      "summary": "Create or rotate an authenticated collector channel for this device.",
      "desc": "Ownership is checked before the app asks the input central. The server chooses the channel id and authenticates batches only against a hash of the collector credential. The whole setup this call returns — credential included — is also saved, encrypted with a key only the input central holds and bound to that credential, so the owner can read it again with GET /api/ambientes/:id/inputs/:collector/setup. The credential does not expire: a device that stays off for weeks reconnects with it. It stops working only when a new configuration rotates it or the channel is removed. A repeated call for the same collector rotates the credential, replaces the saved setup and replaces `monitoring` with the list sent — that is how the monitored scope is edited — so the configuration on the device stops authenticating until it is replaced; a paused channel stays paused. DNS selects the receiver; it never authenticates the device.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        }
      },
      "corpo": {
        "collector": {
          "tipo": "string",
          "desc": "alloy, telegraf, opentelemetry, ncpa, collectd or snmp.",
          "obrigatorio": true
        },
        "monitoring": {
          "tipo": "string[]",
          "desc": "One or more of system (the whole machine), host (CPU, memory, load and uptime), disk, network, mysql and asterisk. `system` already includes host, disk and network, and those three together are stored as `system`. Items the collector cannot collect (see `collects` on the channel) are not measured.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "channel": {
          "tipo": "object",
          "desc": "Channel id, collector, protocol and connection state."
        },
        "setup": {
          "tipo": "object",
          "desc": "Native endpoint and collector credential (`connection`), the collector configuration (`manual`), the agent or collectd side when there is one (`companion`), every file the device needs — `name`, `path`, `owner`, `group`, `mode`, `content` and the `put` command that places it (`files`) —, the by-hand steps for the system the device identification saw — `system` (os, distro, arch, package manager, init), `family` (systemd; foreground, for a Linux without systemd running such as a container; brew; or generic), `packages` (each with the exact install command for that package manager — apt, dnf, yum or zypper — or why there is none), `fresh` (what to run when the package was just installed), `start`, `foreground` (one command per process, without systemd) and `verify` (`steps`) and, on a Linux with systemd and apt, dnf or yum, or on Homebrew, install/verify/remove commands (`installation`) — with `installation.oneLine`, the same install as one line (`curl -fsSL -u '<channel>:<credential>' https://<ingest host>/install.sh | sudo sh`) that fetches the script with this channel's credential. The install ends with a line saying it is installed and running, or the collector's last log lines."
        },
        "generatedAt": {
          "tipo": "string",
          "desc": "When this setup was generated and saved (ISO 8601)."
        }
      },
      "erros": {
        "400": null,
        "401": null,
        "404": null,
        "422": null,
        "503": "The input central is unavailable; nothing was changed."
      },
      "exemplo": "curl -s \"$ORIGIN/api/ambientes/$ID/inputs\" -H \"X-Guest-Token: $HP\" -H \"content-type: application/json\" -d '{\"collector\":\"alloy\",\"monitoring\":[\"system\"]}'",
      "returns": "{ channel, setup, generatedAt }",
      "url": "https://staging.hookpulse.net/api/ambientes/:id/inputs",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/api/ambientes/:id/inputs",
      "auth": "guest",
      "grupo": "Devices",
      "summary": "List this device's collector channels, their state and their live numbers.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        }
      },
      "retorno": {
        "channels": {
          "tipo": "array",
          "desc": "At most 12 channels; credentials are never returned by this route. `status` is receiving (a batch in the last `staleAfterSec`), silent, waiting (this configuration has not sent yet), paused or revoked; `stats` carries cpu, mem, memTotal, disk, rx, tx (bytes/s), load1, uptime and services (mysql, asterisk), null when nothing was measured yet. `collects` lists what this collector can measure (host, disk, network, mysql, asterisk) and `adjustable` is false when its configuration always sends the same set (collectd, SNMP). `historyBatches` is how many received batches the History keeps for the channel (null until the input central has counted them). `uninstall` has, for this device's system, the command that stops and removes the HookPulse collector service and its configuration — and uninstalls the collector package when HookPulse installed it, keeping one the device already had; its last line says what was removed (`remove`) — and the one that checks whether it still runs (`verify`); neither carries the credential, so nothing is rotated; `available` is false when the collector was installed by hand."
        }
      },
      "erros": {
        "401": null,
        "404": null,
        "503": "The input central is unavailable; try again."
      },
      "exemplo": "curl -s \"$ORIGIN/api/ambientes/$ID/inputs\" -H \"X-Guest-Token: $HP\"",
      "returns": "{ channels }",
      "url": "https://staging.hookpulse.net/api/ambientes/:id/inputs",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/api/ambientes/:id/inputs/:collector/setup",
      "auth": "guest",
      "grupo": "Devices",
      "summary": "The saved setup of one collector channel: the configuration in use, its files and steps.",
      "desc": "Returns the setup generated last for this channel — the one whose credential is accepted now — exactly as POST /api/ambientes/:id/inputs returned it, so the files can be copied or installed again without rotating the credential. It carries the credential: only the device owner gets it, one channel per request, never cached; list routes never include it. 404 `setup_not_saved` when the channel was generated before setups were saved, or was removed; 409 `setup_unreadable` when the input central can no longer open it. In both cases generate a new setup with POST /api/ambientes/:id/inputs.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        },
        "collector": {
          "tipo": "string",
          "desc": "alloy, telegraf, opentelemetry, ncpa, collectd or snmp."
        }
      },
      "retorno": {
        "channelId": {
          "tipo": "string",
          "desc": "The channel this setup belongs to."
        },
        "generatedAt": {
          "tipo": "string",
          "desc": "When it was generated (ISO 8601)."
        },
        "setup": {
          "tipo": "object",
          "desc": "The same object as `setup` in POST /api/ambientes/:id/inputs."
        }
      },
      "erros": {
        "401": null,
        "404": "No saved setup for this channel (or not yours); generate one.",
        "409": "The saved setup can no longer be opened; generate a new one.",
        "422": null,
        "503": "The input central is unavailable; try again."
      },
      "exemplo": "curl -s \"$ORIGIN/api/ambientes/$ID/inputs/alloy/setup\" -H \"X-Guest-Token: $HP\"",
      "returns": "{ channelId, generatedAt, setup }",
      "url": "https://staging.hookpulse.net/api/ambientes/:id/inputs/:collector/setup",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/api/ambientes/:id/inputs/:collector/history",
      "auth": "guest",
      "grupo": "Devices",
      "colecao": {
        "porPagina": 25,
        "teto": 100,
        "anda": "page"
      },
      "summary": "The batches this collector sent, newest first — filtered and paginated on the server.",
      "desc": "Every batch the input central received for this channel, as it arrived: when, the protocol, the result (`stored`; `rejected` by the receiver, with the reason; or `discarded` while the channel was paused, when its data is not kept), the answer given to the collector, the size, and how many series and samples it carried. The central keeps the newest batches of each channel up to the limits in `limits` (count, bytes and days); older ones leave as new ones arrive, and removing the channel deletes them. `q` searches metric names — host and service names for NCPA — by opening the batches newest first within a time budget: `search.complete` says whether all candidates were read, and repeating the call continues faster. Open one batch with GET /api/ambientes/:id/inputs/:collector/history/:batch.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        },
        "collector": {
          "tipo": "string",
          "desc": "alloy, telegraf, opentelemetry, ncpa, collectd or snmp."
        }
      },
      "query": {
        "page": {
          "tipo": "int",
          "desc": "Page, from 1 (newest). Beyond the last page answers the last one.",
          "padrao": 1
        },
        "size": {
          "tipo": "int",
          "desc": "Batches per page: 10, 25, 50 or 100.",
          "padrao": 25,
          "valores": [
            "10",
            "25",
            "50",
            "100"
          ]
        },
        "outcome": {
          "tipo": "string",
          "desc": "Only batches with this result.",
          "valores": [
            "stored",
            "rejected",
            "discarded"
          ]
        },
        "since": {
          "tipo": "string",
          "desc": "Only batches received at or after this instant (ISO 8601)."
        },
        "until": {
          "tipo": "string",
          "desc": "Only batches received at or before this instant (ISO 8601)."
        },
        "q": {
          "tipo": "string",
          "desc": "Metric name (or NCPA host/service) contains this text; up to 120 characters."
        }
      },
      "retorno": {
        "channelId": {
          "tipo": "string",
          "desc": "The channel these batches belong to."
        },
        "items": {
          "tipo": "array",
          "desc": "`{id, receivedAt, input, outcome, status, bytes, series, samples, error}`; `series`/`samples` are null when the data was not kept or could not be decoded."
        },
        "page": {
          "tipo": "int",
          "desc": "The page returned."
        },
        "pageSize": {
          "tipo": "int",
          "desc": "Batches per page."
        },
        "total": {
          "tipo": "int",
          "desc": "Batches that match the filters."
        },
        "pages": {
          "tipo": "int",
          "desc": "Pages with those filters (at least 1)."
        },
        "kept": {
          "tipo": "object",
          "desc": "`{batches, bytes, newest, oldest}` kept for this channel, without filters."
        },
        "limits": {
          "tipo": "object",
          "desc": "`{batches, bytes, days}`: how many batches the central keeps per channel."
        },
        "search": {
          "tipo": "object",
          "desc": "Only with `q`: `{q, scanned, candidates, complete}`.",
          "opcional": true
        }
      },
      "erros": {
        "401": null,
        "404": "Unknown device or channel (or not yours).",
        "422": null,
        "503": "The input central is unavailable, or it does not keep batches (`history_unavailable`)."
      },
      "exemplo": "curl -s \"$ORIGIN/api/ambientes/$ID/inputs/alloy/history?size=25&outcome=rejected\" -H \"X-Guest-Token: $HP\"",
      "returns": "{ channelId, items, page, pageSize, total, pages, kept, limits, search? }",
      "url": "https://staging.hookpulse.net/api/ambientes/:id/inputs/:collector/history",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/api/ambientes/:id/inputs/:collector/history/:batch",
      "auth": "guest",
      "grupo": "Devices",
      "summary": "One received batch, decoded: its series with labels and values, or its NCPA checks.",
      "desc": "Remote Write (1.0 and 2.0), OTLP (protobuf or JSON) and NRDP become the same shape: `series` (`name`, `labels`, `points` as `[milliseconds, value]`, and `type`/`unit`/`help` when the collector sent them; OTLP points also carry `resource`, an index into `resources`) or `checks` for NCPA (`host`, `service`, `state` 0–3, `output`, `perfdata`). A value JSON cannot hold comes as a string: `\"NaN\"`, `\"+Inf\"`, `\"-Inf\"` or `\"stale\"`. Very large batches keep the first 5,000 series (`truncated`). `newer`/`older` are the neighbouring batch ids, for moving through the history.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        },
        "collector": {
          "tipo": "string",
          "desc": "alloy, telegraf, opentelemetry, ncpa, collectd or snmp."
        },
        "batch": {
          "tipo": "string",
          "desc": "The batch `id` from the history list.",
          "valores": [
            "1790165043059-0005-s-204-w-5357"
          ]
        }
      },
      "retorno": {
        "channelId": {
          "tipo": "string",
          "desc": "The channel of the batch."
        },
        "item": {
          "tipo": "object",
          "desc": "The list fields plus `contentType` and `contentEncoding` as the collector sent them."
        },
        "content": {
          "tipo": "object",
          "desc": "`{format, series, resources?, checks?, seriesCount, sampleCount, truncated}`; null when the data was not kept.",
          "nulo": true
        },
        "unreadable": {
          "tipo": "string",
          "desc": "Why the kept data could not be decoded; null when it could.",
          "nulo": true
        },
        "newer": {
          "tipo": "string",
          "desc": "The next newer batch id, or null.",
          "nulo": true
        },
        "older": {
          "tipo": "string",
          "desc": "The next older batch id, or null.",
          "nulo": true
        }
      },
      "erros": {
        "401": null,
        "404": "Unknown device or channel, or the batch is no longer kept (`batch_not_found`).",
        "422": null,
        "503": "The input central is unavailable, or it does not keep batches (`history_unavailable`)."
      },
      "exemplo": "curl -s \"$ORIGIN/api/ambientes/$ID/inputs/alloy/history/$BATCH\" -H \"X-Guest-Token: $HP\"",
      "returns": "{ channelId, item, content, unreadable, newer, older }",
      "url": "https://staging.hookpulse.net/api/ambientes/:id/inputs/:collector/history/:batch",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "DELETE",
      "path": "/api/ambientes/:id/inputs/:collector",
      "auth": "guest",
      "grupo": "Devices",
      "summary": "Revokes and removes one collector channel from a device.",
      "desc": "The input central removes only a channel of the same owner. This stops new authenticated input and removes the channel from the device list — also for a channel whose device was already deleted. It does not uninstall or stop the collector on the device (`uninstall.remove` in GET /api/ambientes/:id/inputs is the command for that), and stored measurements remain subject to their retention policy.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        },
        "collector": {
          "tipo": "string",
          "desc": "alloy, telegraf, opentelemetry, ncpa, collectd or snmp."
        }
      },
      "retorno": {
        "ok": {
          "tipo": "boolean",
          "desc": "True after the channel was revoked."
        },
        "deleted": {
          "tipo": "boolean",
          "desc": "True after the channel was removed from the device."
        },
        "channel": {
          "tipo": "string",
          "desc": "The revoked channel id."
        }
      },
      "erros": {
        "401": null,
        "404": null,
        "422": null,
        "503": "The input central is unavailable; nothing was changed."
      },
      "exemplo": "curl -s -XDELETE \"$ORIGIN/api/ambientes/$ID/inputs/alloy\" -H \"X-Guest-Token: $HP\"",
      "returns": "{ ok, deleted, channel }",
      "url": "https://staging.hookpulse.net/api/ambientes/:id/inputs/:collector",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "PATCH",
      "path": "/api/ambientes/:id/inputs/:collector",
      "auth": "guest",
      "grupo": "Devices",
      "summary": "Pauses or resumes one collector channel.",
      "desc": "Pausing keeps the channel, its credential and its configuration: batches the collector sends while paused are accepted and discarded, so nothing is stored or measured and the collector does not retry in a loop. Resuming stores the next batch again. The input central changes only a channel of the same owner, including one whose device was already deleted.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        },
        "collector": {
          "tipo": "string",
          "desc": "alloy, telegraf, opentelemetry, ncpa, collectd or snmp."
        }
      },
      "corpo": {
        "active": {
          "tipo": "boolean",
          "desc": "false pauses, true resumes.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "ok": {
          "tipo": "boolean",
          "desc": "True after the change."
        },
        "channel": {
          "tipo": "object",
          "desc": "The channel with its new `status` and `stats`."
        }
      },
      "erros": {
        "400": null,
        "401": null,
        "404": null,
        "422": null,
        "503": "The input central is unavailable; nothing was changed."
      },
      "exemplo": "curl -s -XPATCH \"$ORIGIN/api/ambientes/$ID/inputs/alloy\" -H \"X-Guest-Token: $HP\" -H \"content-type: application/json\" -d '{\"active\":false}'",
      "returns": "{ ok, channel }",
      "url": "https://staging.hookpulse.net/api/ambientes/:id/inputs/:collector",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "GET",
      "path": "/api/inputs/serie",
      "auth": "guest",
      "grupo": "Devices",
      "summary": "Every collector channel of the caller, with state and live numbers, in one response.",
      "desc": "One call for the whole fleet — one query to the time-series store for all channels, never one per device. It is the same function the dashboard subscribes to over the socket (`/api/inputs/serie`), where the input central then pushes `inputs.updated` and `inputs.removed` events; integrators get the same data here without the socket.",
      "retorno": {
        "channels": {
          "tipo": "array",
          "desc": "Channels as in GET /api/ambientes/:id/inputs, plus `deviceName`/`deviceGroup` from this app and `deviceMissing` when the device was deleted but the channel was left behind."
        },
        "at": {
          "tipo": "string",
          "desc": "When the snapshot was taken (ISO 8601)."
        }
      },
      "erros": {
        "401": null,
        "503": "The input central is unavailable; try again."
      },
      "exemplo": "curl -s \"$ORIGIN/api/inputs/serie\" -H \"X-Guest-Token: $HP\"",
      "returns": "{ channels, at }",
      "url": "https://staging.hookpulse.net/api/inputs/serie",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "PATCH",
      "path": "/api/ambientes/:id",
      "auth": "guest",
      "grupo": "Devices",
      "summary": "Renames a device, and its measurements along with it.",
      "desc": "One machine has one name. The measurements carry a copy of it, so renaming here renames them too — leaving the old name on them would show the same machine twice in a list that groups by device. Its collector channels take the new name and group too in `GET /api/coletas` (`inputs[]`), and the group decides which dashboard tab the device is under. Send only what changes; what you omit stays. The detected environment is never rewritten: it is evidence of what that machine reported.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        }
      },
      "corpo": {
        "nome": {
          "tipo": "string",
          "desc": "New device name, 1–60 characters."
        },
        "grupo": {
          "tipo": "string",
          "desc": "New group, up to 40 characters; empty clears it."
        }
      },
      "retorno": {
        "ambiente": {
          "tipo": "Ambiente",
          "desc": "The device after the change."
        }
      },
      "erros": {
        "400": "nome or grupo outside the accepted shape.",
        "401": null,
        "404": null
      },
      "exemplo": "curl -s -XPATCH \"$ORIGIN/api/ambientes/$ID\" -H 'content-type: application/json' -H \"X-Guest-Token: $HP\" -d '{\"nome\":\"web-01\",\"grupo\":\"production\"}'",
      "returns": "{ ambiente{id,nome,grupo,criado_em,expira_em,detectado_em,estado,dados,medidas,passos,sonda} }",
      "url": "https://staging.hookpulse.net/api/ambientes/:id",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "DELETE",
      "path": "/api/ambientes/:id",
      "auth": "guest",
      "grupo": "Devices",
      "summary": "Removes a device and every measurement on it, in one call.",
      "desc": "One call, not one per measurement — the same reason `GET /api/coletas/serie` exists. What goes away is the ownership: your list, the authority to read those series, and the slots those measurements held, so the device can be registered again. Readings already stored at the ingest origin stay there until their own retention, and the machine keeps sending: the symbols are signed and stand on their own there. That is why the answer carries `remover`, one command per measurement that had a cron line. The device's collector channels are revoked first; if the input central cannot confirm that, nothing is deleted (503) and the call can be repeated, so no channel is left receiving data without a device.",
      "params": {
        "id": {
          "tipo": "string",
          "desc": "Device environment UUID v4; the report URL is a one-hour write capability.",
          "valores": [
            "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
          ]
        }
      },
      "retorno": {
        "ok": {
          "tipo": "bool",
          "desc": "true when the device was removed."
        },
        "id": {
          "tipo": "string",
          "desc": "The device that was removed."
        },
        "deleted": {
          "tipo": "bool",
          "desc": "Always true; the rows are gone, not flagged."
        },
        "coletas": {
          "tipo": "int",
          "desc": "How many measurements were removed with it."
        },
        "canais": {
          "tipo": "int",
          "desc": "How many collector channels were revoked with it."
        },
        "remover": {
          "tipo": "string[]",
          "desc": "One command per measurement that had a cron line, to take those lines out of the device's crontab. Empty when none had one."
        }
      },
      "erros": {
        "401": null,
        "404": null,
        "503": "The input central did not confirm; nothing was deleted."
      },
      "exemplo": "curl -s -XDELETE \"$ORIGIN/api/ambientes/$ID\" -H \"X-Guest-Token: $HP\"",
      "returns": "{ ok, id, deleted, coletas, canais, remover }",
      "url": "https://staging.hookpulse.net/api/ambientes/:id",
      "auth_detail": "Guest token (`POST /api/guest`) in `X-Guest-Token: hp_…` or `Authorization: Bearer hp_…`. A signed-in account (cookie) also works and takes precedence."
    },
    {
      "method": "POST",
      "path": "/api/credito",
      "auth": "none",
      "summary": "Top up prepaid credit: pay once with x402 and get the token that debits on any API of the house.",
      "grupo": "Credit",
      "query": {
        "usd": {
          "tipo": "int",
          "desc": "Package: 1, 5, 10 ou 25 dollars.",
          "obrigatorio": true
        }
      },
      "retorno": {
        "token": {
          "tipo": "string",
          "desc": "Bearer token for the balance (`cred_…`). Shown ONCE — it cannot be recovered."
        },
        "saldo_usd": {
          "tipo": "string",
          "desc": "Credited balance."
        },
        "guarde": {
          "tipo": "string",
          "desc": "Warning that the token is the bearer of the credit."
        },
        "usar": {
          "tipo": "string",
          "desc": "How to present the token on paid routes."
        },
        "saldo_em": {
          "tipo": "string",
          "desc": "Where to check balance and statement."
        }
      },
      "erros": {
        "400": "Package outside the list (1, 5, 10 ou 25).",
        "402": "Unpaid — the body carries the x402 `accepts[]`."
      },
      "exemplo": "curl -s -XPOST '$ORIGIN/api/credito?usd=10'",
      "returns": "{ token, saldo_usd, guarde, usar, saldo_em }",
      "url": "https://staging.hookpulse.net/api/credito",
      "auth_detail": "Public, no credential."
    },
    {
      "method": "GET",
      "path": "/api/credito",
      "auth": "credito",
      "summary": "Credit balance and statement — the latest movements, without returning the token.",
      "grupo": "Credit",
      "retorno": {
        "saldo_micros": {
          "tipo": "int",
          "desc": "Balance in micro-dollars (1e-6 USD)."
        },
        "saldo_usd": {
          "tipo": "string",
          "desc": "Formatted balance."
        },
        "criado_em": {
          "tipo": "string",
          "desc": "When the credit was opened."
        },
        "movimentos": {
          "tipo": "object[]",
          "desc": "Recent credits and debits, with product and resource."
        }
      },
      "erros": {
        "401": "No token, or unknown token."
      },
      "exemplo": "curl -s $ORIGIN/api/credito -H 'Authorization: Bearer cred_…'",
      "returns": "{ saldo_micros, saldo_usd, criado_em, movimentos }",
      "url": "https://staging.hookpulse.net/api/credito",
      "auth_detail": "Prepaid credit token in `Authorization: Bearer cred_…` (or the `X-Credito` header). Not an account: it is a bearer of balance."
    },
    {
      "method": "GET",
      "path": "/api/pricing",
      "auth": "none",
      "grupo": "Discovery",
      "summary": "Current prices and free allowances.",
      "retorno": {
        "product": {
          "tipo": "string",
          "desc": "Product name."
        },
        "quota": {
          "tipo": "PaymentQuota",
          "desc": "Public allowances and current list prices; not personal usage."
        },
        "pricing": {
          "tipo": "string",
          "desc": "Absolute URL of the current price list."
        },
        "billing": {
          "tipo": "string",
          "desc": "Absolute URL of payment discovery or the existing billing summary."
        },
        "api_index": {
          "tipo": "string",
          "desc": "Absolute URL of the API catalog."
        }
      },
      "erros": {
        "405": "Use GET or HEAD."
      },
      "exemplo": "curl -s $ORIGIN/api/pricing",
      "returns": "{ product, quota{free,paid,how_to_pay,live,free_now?,trial?}, pricing, billing, api_index }",
      "url": "https://staging.hookpulse.net/api/pricing",
      "auth_detail": "Public, no credential."
    }
  ],
  "quota": {
    "free": [
      {
        "o_que": "dead-man endpoints",
        "limite": "10 per owner",
        "janela": null
      },
      {
        "o_que": "minimum interval at no cost",
        "limite": "90s",
        "janela": null
      },
      {
        "o_que": "ingest (`/in/:id`)",
        "limite": "no charge per ping",
        "janela": null
      },
      {
        "o_que": "webhook alert (`alert_url`)",
        "limite": "no quota — it is a fetch of ours",
        "janela": null
      },
      {
        "o_que": "e-mail alert (`alert_to`)",
        "limite": "1 registration",
        "janela": null
      },
      {
        "o_que": "status dashboard by URL (JSON and RSS)",
        "limite": "no quota",
        "janela": null
      }
    ],
    "paid": [
      {
        "o_que": "endpoint beyond the allowance",
        "price_usd": 0.1
      },
      {
        "o_que": "interval below the free minimum",
        "price_usd": 0.05
      },
      {
        "o_que": "e-mail alert registration beyond the 1st",
        "price_usd": 0.1
      }
    ],
    "how_to_pay": "Past the allowance → **402** with `accepts[]` (x402, USDC on Base). Pay and repeat the same call with `X-PAYMENT`.",
    "live": "https://staging.hookpulse.net/api/billing",
    "trial": {
      "days": 90,
      "how": "Sign in with your account at /conta/global (browser: e-mail code or link, password or Google) → 90 days without the usage paywall, counted from the account's first use of HookPulse (extra endpoints and fast intervals; e-mail alerts beyond the 1st stay $0.10)"
    }
  },
  "mcp": {
    "endpoint": "https://staging.hookpulse.net/mcp",
    "transport": "streamable-http",
    "tools": 41,
    "note": "Plugs straight into the MCP client; nothing to install. Tools = the operations below."
  },
  "mcp_tools": [
    "list_environments",
    "create_environment",
    "get_environment",
    "create_device_input",
    "list_device_inputs",
    "get_device_input_setup",
    "list_device_input_history",
    "get_device_input_batch",
    "delete_device_input",
    "set_device_input_active",
    "list_input_fleet",
    "report_environment",
    "list_collections",
    "create_collection",
    "get_collection_series",
    "update_collection",
    "delete_collection",
    "update_environment",
    "delete_environment",
    "api_index",
    "health",
    "create_guest",
    "list_endpoints",
    "create_endpoint",
    "import_crontab",
    "get_endpoint",
    "delete_endpoint",
    "list_events",
    "ping_ingest",
    "signal_start",
    "signal_fail",
    "signal_exit_code",
    "ingest_key",
    "ingest_key_rotate",
    "ping_slug",
    "billing",
    "status_feed_url",
    "status_feed_rotate",
    "list_templates",
    "contact",
    "pricing"
  ],
  "quickstart": [
    "TOKEN=$(curl -s -XPOST https://staging.hookpulse.net/api/guest -H 'content-type: application/json' -d '{}' | jq -r .token)",
    "curl -s -XPOST https://staging.hookpulse.net/api/endpoints -H \"content-type: application/json\" -H \"X-Guest-Token: $TOKEN\" -d '{\"name\":\"demo\",\"interval_sec\":900}'",
    "curl -s https://staging.hookpulse.net/api/billing"
  ]
}