API documentation
The portscan.com API checks open TCP ports on a server programmatically. No API key, no authentication, and no signup required. The scan target is always the IP address of the server making the API request, determined from the connection itself. This makes the API suitable for servers checking their own port exposure, CI/CD pipelines verifying firewall rules, and monitoring scripts confirming expected services are reachable.
Both IPv4 and IPv6 are supported. Port 25 (SMTP) is included in every scan so mail reachability is visible in the same result set as the other ports, the status reflects what can be reached from outside, which matters for server-to-server SMTP and deliverability checks.
The API only scans the public IP address from which the API request reaches the service. Arbitrary target scanning is not supported. API use is logged and rate-limited for abuse prevention.
Quick start
Run from the server to be scanned:
# Start a fast scan curl -X POST https://api.portscan.com/v1/fast # Poll for results curl https://api.portscan.com/v1/fast
Or with v2, a single request starts the scan and streams results over Server-Sent Events, no polling:
# Start and stream in one request curl -N https://api.portscan.com/v2/fast
The API scans the IP address the curl request originates from. Running these commands from a local workstation scans the workstation’s public IP. Running them from a cloud server scans the server’s IP.
Endpoints
Polling (v1)
POST to start a scan, then GET to poll for
results until status is terminal.
| Method | Path | Description |
|---|---|---|
| POST | /v1/fast | Start a fast scan (32 ports) |
| POST | /v1/deep | Start a deep scan (65,535 ports) |
| GET | /v1/fast | Poll fast scan results |
| GET | /v1/deep | Poll deep scan results |
Streaming (v2)
A single GET starts the scan (if none is active for your IP) and streams progress over Server-Sent Events. No POST needed; to start without a stream, use the v1 POST.
| Method | Path | Description |
|---|---|---|
| GET | /v2/fast | Start and stream a fast scan (SSE) |
| GET | /v2/deep | Start and stream a deep scan (SSE) |
Streaming events (v2)
v1 uses polling: POST to
start, then GET repeatedly until
status is terminal.
v2 is a single
Server-Sent Events request: the GET
starts the scan (if one is not already running
for your IP) and streams progress on one open
connection, instead of N poll requests. v1 is
unchanged; pick whichever fits your client.
# one request: start and stream curl -N https://api.portscan.com/v2/deep
To start a scan without holding a stream open,
use POST /v1/{fast,deep}; a later
GET /v2/{fast,deep} attaches to
the same in-flight scan.
The stream emits these named events, each a
JSON data: payload:
-
scan:queued: accepted; includestotal_chunks,eta_seconds. -
scan:progress: on each advance;percent,new_ports(delta) and cumulative open/filtered counts. -
scan:complete: terminal success; fullports_open,service_banners,duration_ms. scan:failed: terminal failure;partial_resultsis true when the host was too slow to finish the full range (salvaged ports arrive in the precedingscan:progress).
Comment lines (: keep-alive) are
sent periodically so intermediaries do not
drop an idle connection. The stream closes
after the terminal event.
MCP server
AI agents can scan as a native tool over the
Model Context Protocol (Streamable HTTP) at
https://api.portscan.com/mcp -
tools fast_scan and
deep_scan, no key. See the
MCP
documentation for client setup and how it
works.
Scan duration
Typical wall-clock time after the
scan actually starts is around
10-20 seconds for a fast
scan and 60-300
seconds for a deep scan. When
multiple scans run in parallel, durations
grow accordingly. The
queue_position field in the
poll response indicates how many scans are
ahead.
The eta_seconds field in the
queued response is an
indicative hint for
polling intervals, not a guarantee of
total time from POST to completion. Poll
until status is
complete or
failed.
Responses
200 Scan submitted
Returned by POST when a scan is queued.
{
"ip": "203.0.113.42",
"scan_type": "fast",
"status": "queued",
"eta_seconds": 20
}
eta_seconds is a rough lower
bound for the scanning phase; real-world
duration is often near the Scan
duration targets above but can
vary with load and network conditions.
If a scan for this IP and scan type is
already in flight, POST returns the same
200 shape with status set to
queued or running
and chunks_complete /
total_chunks fields reflecting
the ongoing scan. Duplicate submissions
coalesce onto the existing scan rather than
starting a new one.
200 Scan complete
Returned by GET when results are ready.
{
"ip": "203.0.113.42",
"scan_type": "fast",
"status": "complete",
"ports_open": [
{"port": 22, "state": "open", "service": "SSH", "banner": null},
{"port": 80, "state": "open", "service": "HTTP", "banner": null},
{"port": 443, "state": "open", "service": "HTTPS", "banner": null}
],
"ports_filtered": [],
"service_banners": {},
"ptr_record": "mail.example.com",
"duration_ms": 6231,
"completed_at": 1711940665,
"expires_at": 1712027200
}200 Scan failed
Returned by GET when a scan did not complete.
{
"ip": "203.0.113.42",
"scan_type": "deep",
"status": "failed",
"chunks_complete": 28,
"total_chunks": 32,
"fail_reason": "timeout"
}200 No scan found
Returned by GET when no scan exists for this IP.
{
"ip": "203.0.113.42",
"scan_type": "fast",
"status": "none"
}Error responses
| Status | Error | Description |
|---|---|---|
| 403 | not_available | IP is opted out or restricted |
| 422 | invalid_target | IP is RFC 1918, loopback, or otherwise non-routable |
| 429 | rate_limited | Too many requests. Check Retry-After header. |
Response headers
| Header | When | Value |
|---|---|---|
| Retry-After | On 429 responses | Seconds until rate limit resets |
| X-RateLimit-Limit | All responses | Maximum requests in the current window |
| X-RateLimit-Remaining | All responses | Requests remaining in the current window |
| X-RateLimit-Reset | All responses | Unix timestamp when the rate limit resets |
| X-Scan-Duration-Ms | Completed scan GET responses | Scan duration in milliseconds |
Webhook
Pass a URL as the X-Callback-Url header to receive a POST notification when the scan completes or fails.
curl -X POST https://api.portscan.com/v1/fast \ -H "X-Callback-Url: https://myserver.com/hook"
The webhook fires on both completion and failure. Webhook and polling are fully complementary and work regardless of whether a callback URL was provided.
Rate limits
| Scope | Limit | Window |
|---|---|---|
| Fast scan | 2000 per ASN | 1 hour |
| Deep scan | 1 per IP, 200 per ASN | 5 minutes (IP), 1 hour (ASN) |
| Edge limit | 30 requests per IP | 1 minute |
Rate-limited responses return HTTP 429 with a Retry-After header.
Common use cases
Server hardening check. After configuring a firewall, run a fast scan from the server to confirm only intended ports are open. Add the curl command to a post-deployment script.
Mail server verification. Check whether port 25 is open and reachable from outside the network. The result reflects the same view other mail servers get when delivering to the host.
CI/CD integration. Add a port scan step to deployment pipelines. POST to start a scan, poll until complete, and fail the pipeline if unexpected ports are found open.
Monitoring. Periodically check a server’s port exposure with a cron job. Use the webhook callback to receive results without polling.
Limitations
The API scans the IP of the requesting client only. There is no parameter for specifying a different target IP. The IP address always comes from the requester’s connection, is enforced at the server level and cannot be bypassed. The API is for self-checking (a host or pipeline testing its own egress IP), not for scanning third-party infrastructure.
UDP scanning is not supported. All scans are TCP only.
Scan results expire after 24 hours. Poll for results within this window.
Terms and policies
Usage of the API is subject to the portscan.com Terms of Service and Privacy Policy. For abuse reports or questions, see the Abuse contact page. To opt out an IP or range, visit Opt-out.