External port visibility for agents and servers

Like "what is my IP", but for visible TCP ports.

ifport.io checks which TCP ports are publicly visible on the observed request-source path that contacted the service.

Usage

curl https://ifport.io
curl https://ifport.io/json
curl "https://ifport.io/report"
curl https://ifport.io/check
curl https://ifport.io/plain
curl "https://ifport.io/gate?policy=strict"
curl "https://ifport.io/gate?expect_open=443"
curl "https://ifport.io/json?intent=web-deploy"
curl https://ifport.io/network-path
curl https://ifport.io/status
curl https://ifport.io/openapi.json
curl https://ifport.io/llms.txt
curl https://ifport.io/privacy
curl https://ifport.io/acceptable-use
curl "https://ifport.io/json?profile=default"
curl "https://ifport.io/check?ports=22,80,443,5432"

Important

The result describes the request-source path IfPort observed. If that path is public, the scan describes that public edge; if it is private, proxied, or sandboxed, use /network-path before host-level conclusions.

curl ifport.io is the fastest way to check visible ports from the current environment.

No-parameter scans use profile=default with a compact request-source profile: 22,80,443,3000,8080,8443. Use profile=top1000 for explicit broad verification.

If the request is made through NAT, VPN, proxy, CI/CD, cloud runtime, or an LLM sandbox, the result may not describe your local machine.

The default root endpoint is a compact result brief. It is deterministic when format is set: /?format=json, /?format=markdown, and /?format=plain bypass browser/CLI heuristics and force the requested representation.

Use /report for the extended explanation of the current bounded result.

/json is the canonical machine response. Omit optional projection parameters for new integrations; the default JSON projection is the stable core contract.

For ready-to-say agent handoff copy, use /json?expand=agent_handoff; it adds user_message, next_action, next_command, and confidence_note without changing the default core projection.

Stable machine contract

The following fields are guaranteed stable in 0.x. Agents should treat this as the core bounded parse set; additional response fields may be added and should be ignored when unknown.

Agents should parse the core response first. Use /gate for bounded CI verdicts and /network-path when source attribution is unclear.

/gate is compact by default: it returns snapshot_id, snapshot_sha256, verdict, reason_code, explicit partial/completed coverage fields, action.next_command, and CI fields, but intentionally omits full_result. Add include_full_result=true only when the clean core scan payload is needed; it does not include legacy presentation, support, social, or operator-card fields.

HTTP 200 requires every selected port to complete. A partial scan returns HTTP 409 with reason_code=partial_scan, partial_reason, completed_ports_count, and not_attempted_ports. Every 409 body remains structured with status=blocked, verdict, snapshot_id, snapshot_sha256, coverage, action, note, and the compatible error.code=policy_gate_failed. A non-public observed source returns HTTP 400 with error.code=invalid_request and /network-path guidance before a scan or gate verdict is created.

Gate examples: /gate?expect_open=443 asserts an exact visible-port baseline, /gate?policy=web&ports=22,80,443 checks a bounded web policy over explicit ports, and /json?intent=web-deploy returns the same request-source result with intent diagnosis. A pass means only the selected bounded policy passed for this request path; it is not a security assessment.

CI gate quickstart

Run /gate from the same runner, container, VPS, or operator egress path whose exposure you want to prove. curl -f fails the job on HTTP 409.

# GitLab CI
ifport_gate:
  image: curlimages/curl:8.8.0
  stage: test
  script:
    - curl -fsS "https://ifport.io/gate?expect_open=443"

# GitHub Actions
- name: IfPort exposure gate
  run: curl -fsS "https://ifport.io/gate?expect_open=443"

If attribution is not public, scan endpoints fail closed. Call /network-path from the same job and fix the request path before retrying the gate.

Example JSON response

GET /json returns a request-source result like this. Run the command from the exact network path you want to validate.

{
  "generated_at": "2000-01-01T00:00:00Z",
  "limitations": [
    "This result describes the public request-source path.",
    "If the request passed through NAT, VPN, proxy, CI/CD, cloud runtime, or an LLM sandbox, the result may not describe your local machine.",
    "Unchecked ports are not assessed by the selected profile or explicit port list.",
    "Policy verdicts apply only to the selected bounded policy and selected scan vantage; they are not a security assessment."
  ],
  "links": {
    "network_path": "https://ifport.io/network-path",
    "report": "https://ifport.io/report"
  },
  "meta": {
    "build_id": "example-build-not-live",
    "release_sha": "image-example-not-live-0123456789abcdef0123456789abcdef",
    "request_id": "req_0123456789abcdef0123456789abcdef"
  },
  "observed_source": {
    "address_family": "ipv4",
    "attribution_confidence": "high",
    "ip": "203.0.113.42",
    "scope": "public"
  },
  "policy": {
    "name": "strict",
    "unexpected_open_ports": [],
    "verdict": "pass"
  },
  "results": {
    "closed": [],
    "errors": [],
    "filtered_or_timeout": [
      22,
      80,
      443,
      3000,
      8080,
      8443
    ],
    "open": []
  },
  "scan": {
    "completed_ports": [
      22,
      80,
      443,
      3000,
      8080,
      8443
    ],
    "duration_ms": 326,
    "method": "tcp_syn",
    "method_counts": {
      "tcp_syn": 6
    },
    "not_attempted_ports": [],
    "partial": false,
    "partial_reason": null,
    "ports_checked": [
      22,
      80,
      443,
      3000,
      8080,
      8443
    ],
    "profile": "default",
    "speed": "balanced",
    "strategy": "syn_with_connect_fallback",
    "type": "tcp_syn_or_connect",
    "vantage": {
      "count": 1,
      "id": "primary",
      "region": "eu"
    }
  },
  "schema_version": "1.0",
  "service": "ifport.io"
}

Evidence and saved results

Default scans are ephemeral. Use /evidence when you need a portable proof pack for a ticket, incident review, CI handoff, or audit note.

Evidence includes a snapshot fingerprint and a /verify payload. Public Ed25519 verification keys are published at /verify/keys.

Create saved result permalinks only explicitly with POST /results or /json?persist=true. Saved result pages live under /result/{token}, are marked noindex,noarchive, and can be deleted with the returned delete token via Authorization: Bearer ifp_... or X-IfPort-Delete-Token. Delete tokens are never accepted in query parameters.

/trust publishes privacy-safe current-process scan execution quality and aggregate worker registry state. quality.scope=since_process_start excludes cache responses, resets whenever the API process restarts, advances only in completed 10-attempt batches, and keeps sample counts, ratios, and percentiles null before the first batch. reliability.scope=current_process is not an availability SLA, spare-capacity signal, or profile-compatibility guarantee.

/trust historical counters are scoped to explicit saved-result events only. Default ephemeral scans are not persisted and are not included in transparency.last_24h or aggregate_exposure.scans_24h.

Support boundary

/support and /donate are no-scan support surfaces. They keep funding and contacts separate from request-source scan results, MCP output, and plain port lists.

When no payment provider is configured, /donate stays available as a provider-pending page and JSON context. Monthly infrastructure target: $15.

Payment never changes scan access, speed, request-source scan scope, rate limits, or target permissions. General troubleshooting support is not provided for free public scans.

MCP integration

GET /mcp intentionally returns 405. Streamable HTTP clients use POST /mcp for JSON-RPC.

Hosted remote MCP checks the MCP client or platform egress path. For a local agent, CI runner, VPS, container, or workstation path, call /json or /gate from that environment; hosted MCP does not inspect a different machine.

Send Accept: application/json, text/event-stream and MCP-Protocol-Version: 2025-11-25. Browser-based clients must send a trusted Origin; send MCP-Session-Id only if a future initialize response returns one.

Lifecycle: call initialize first, send notifications/initialized, then use tools/list and tools/call.

Successful POST /mcp lifecycle, discovery, tool-result, and tool-error responses use Cache-Control: private, no-store; HTTP transport and JSON-RPC parse or validation errors use Cache-Control: no-store.

The exposed tool is check_request_source_ports. tools/list returns its inputSchema and outputSchema; tools/call accepts profile, ports, speed, intent, policy, and expect_open, and rejects arbitrary ip, host, target, CIDR, URL, domain, source-IP search, and common alias arguments such as hostname or target_ip.

curl -fsS -X POST https://ifport.io/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  --data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"ifport-client","version":"1.0"}}}'

curl -fsS -X POST https://ifport.io/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  --data '{"jsonrpc":"2.0","method":"notifications/initialized"}'

npx -y "@modelcontextprotocol/inspector@0.22.0" \
  --cli https://ifport.io/mcp \
  --transport http \
  --method tools/list

npx -y "@modelcontextprotocol/inspector@0.22.0" \
  --cli https://ifport.io/mcp \
  --transport http \
  --method tools/call \
  --tool-name check_request_source_ports \
  --tool-arg profile=default \
  --tool-arg speed=balanced

Operational contract

Error responses use {"error":{"code":"invalid_request|invalid_ports|invalid_profile|invalid_format|invalid_view|invalid_result_token|invalid_delete_token|forbidden|method_not_allowed|not_found|policy_gate_failed|endpoint_gone|persisted_result_not_found|rate_limited|service_unavailable","message":"...","request_id":"req_..."}}. Their server-generated X-Request-Id matches error.request_id. Request-source boundary violations and malformed POST /verify JSON return invalid_request. Delete tokens are rejected as invalid_delete_token when sent in query parameters instead of headers. Authorization and endpoint-policy rejections use forbidden; edge-owned unknown routes use not_found; retired routes use endpoint_gone; missing saved results use persisted_result_not_found. 405 responses return method_not_allowed. 409 gate failures keep policy_gate_failed. 429 responses use rate_limited and include Retry-After, RateLimit-Policy, RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. Operator safe-mode and dependency outages use service_unavailable. Malformed POST /mcp JSON follows JSON-RPC instead, returns code -32700 with a null id, and carries the matching correlation value in error.data.request_id.

Scan rate-limit classes are per observed request-source IP and route: scan=30/min, plain=60/min, custom=10/min, deep=3/min, and top1000=3/min. Class priority is deep, then custom, then top1000, then plain, then scan.

Scan cache keys include observed source IP, profile or custom ports, speed, policy, intent, and expect_open. format and view only change rendering/projection.

Default speed: no-parameter request-source scans use balanced across root, JSON, report, gate, and plain routes; top1000 uses fast unless speed is explicit.

Source attribution uses trusted edge headers only after edge sanitization; otherwise it falls back to the transport source. Use /network-path as the diagnostic source of truth.

No-scan retrievability endpoints also accept HEAD for status, cache, build, and ETag metadata. Scan routes such as /json, /plain, /check, /report, /gate, and /evidence remain GET-only.

The compact public surface is /, /json, /check, /plain, /report, /gate, /network-path, /status, /trust, /openapi.json, /mcp, and /llms.txt. Privacy and acceptable-use policy live at /privacy and /acceptable-use.

Migration notes

Compatibility details, including deprecated aliases, retired funding surfaces, and optional advanced artifacts, live in /openapi-compat.json. Legacy /openapi-extended.json redirects there.

Docs · OpenAPI · Compat OpenAPI · MCP · LLMs · Status · Trust · Support

build_id: 2026.07.13.1248 · release_sha: a5577414420e