{
  "openapi": "3.1.0",
  "info": {
    "title": "portscan.com API",
    "version": "1.0.0",
    "description": "Free port scanner API. Scans the IP address of the requesting server. No API key required.",
    "contact": {
      "name": "Abuse contact",
      "email": "abuse@portscan.com",
      "url": "https://portscan.com/abuse"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://portscan.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://api.portscan.com"
    }
  ],
  "paths": {
    "/v1/fast": {
      "post": {
        "summary": "Start a fast scan (32 ports)",
        "operationId": "startFastScan",
        "parameters": [
          {
            "name": "X-Callback-Url",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "Webhook URL for completion"
          }
        ],
        "responses": {
          "200": {
            "description": "Scan queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanQueued"
                }
              }
            }
          },
          "403": {
            "description": "IP opted out",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid target IP",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds until reset"
              }
            }
          }
        }
      },
      "get": {
        "summary": "Poll fast scan results",
        "operationId": "pollFastScan",
        "responses": {
          "200": {
            "description": "Scan result or status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanResult"
                }
              }
            }
          }
        }
      }
    },
    "/v1/deep": {
      "post": {
        "summary": "Start a deep scan (65,535 ports)",
        "operationId": "startDeepScan",
        "parameters": [
          {
            "name": "X-Callback-Url",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "Webhook URL for completion"
          }
        ],
        "responses": {
          "200": {
            "description": "Scan queued",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanQueued"
                }
              }
            }
          },
          "403": {
            "description": "IP opted out"
          },
          "422": {
            "description": "Invalid target IP"
          },
          "429": {
            "description": "Rate limited"
          }
        }
      },
      "get": {
        "summary": "Poll deep scan results",
        "operationId": "pollDeepScan",
        "responses": {
          "200": {
            "description": "Scan result or status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanResult"
                }
              }
            }
          }
        }
      }
    },
    "/v2/fast": {
      "get": {
        "summary": "Start and stream a fast scan (SSE)",
        "operationId": "streamFastScan",
        "description": "Single-request streaming. Starts a scan for the caller IP (if none is active) and streams progress over Server-Sent Events: scan:queued, scan:progress, then a terminal scan:complete or scan:failed. A blocked start (invalid target, opt-out, rate limit) returns the JSON error instead.",
        "responses": {
          "200": {
            "description": "SSE event stream",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/v2/deep": {
      "get": {
        "summary": "Start and stream a deep scan (SSE)",
        "operationId": "streamDeepScan",
        "description": "Single-request streaming. Starts a scan for the caller IP (if none is active) and streams progress over Server-Sent Events: scan:queued, scan:progress, then a terminal scan:complete or scan:failed. A blocked start (invalid target, opt-out, rate limit) returns the JSON error instead.",
        "responses": {
          "200": {
            "description": "SSE event stream",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ScanQueued": {
        "type": "object",
        "properties": {
          "ip": {
            "type": "string"
          },
          "scan_type": {
            "type": "string",
            "enum": [
              "fast",
              "deep"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running"
            ],
            "description": "queued for a fresh scan, or running when coalesced onto an in-flight scan for this IP"
          },
          "eta_seconds": {
            "type": "integer"
          },
          "chunks_complete": {
            "type": "integer"
          },
          "total_chunks": {
            "type": "integer"
          }
        }
      },
      "ScanResult": {
        "type": "object",
        "properties": {
          "ip": {
            "type": "string"
          },
          "scan_type": {
            "type": "string",
            "enum": [
              "fast",
              "deep"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "running",
              "complete",
              "incomplete",
              "failed",
              "none"
            ],
            "description": "'incomplete' = host too slow or filtered to finish the full range; ports_open holds what was found, partial=true. Never treat as a complete result."
          },
          "ports_open": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Port"
            }
          },
          "ports_filtered": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Port"
            }
          },
          "service_banners": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "ptr_record": {
            "type": "string",
            "nullable": true
          },
          "queue_position": {
            "type": "integer",
            "description": "Number of scans ahead of this one (only present while queued or running)"
          },
          "duration_ms": {
            "type": "integer"
          },
          "completed_at": {
            "type": "integer"
          },
          "expires_at": {
            "type": "integer"
          },
          "fail_reason": {
            "type": "string",
            "nullable": true
          },
          "partial": {
            "type": "boolean",
            "description": "True when status is incomplete: results are partial, not a full scan of the range."
          },
          "chunks_complete": {
            "type": "integer"
          },
          "total_chunks": {
            "type": "integer"
          }
        }
      },
      "Port": {
        "type": "object",
        "properties": {
          "port": {
            "type": "integer"
          },
          "state": {
            "type": "string",
            "enum": [
              "open",
              "closed",
              "filtered"
            ]
          },
          "service": {
            "type": "string",
            "nullable": true
          },
          "banner": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          },
          "retry_after": {
            "type": "integer"
          }
        }
      }
    }
  }
}