{
  "openapi": "3.0.0",
  "paths": {
    "/v1/lookup": {
      "post": {
        "operationId": "LookupController_createLookup",
        "summary": "Run a lookup",
        "description": "Send one of `query` (auto-detected), `email`, or `phone`. Returns immediately with a `lookupId` to poll, or pass `?wait=true` to block until the full result is ready.",
        "parameters": [
          {
            "name": "wait",
            "required": false,
            "in": "query",
            "description": "Set to `true` to wait for the full result.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LookupRequestDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "With `?wait=true`, the full result. Without it, a job handle to poll.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/LookupResponseDto"
                    },
                    {
                      "$ref": "#/components/schemas/PendingLookupDto"
                    }
                  ]
                }
              }
            }
          },
          "402": {
            "description": "Free trial used up, or monthly plan limit reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorDto"
                }
              }
            }
          },
          "422": {
            "description": "No query, email, or phone was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorDto"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorDto"
                }
              }
            }
          }
        },
        "tags": [
          "Lookup"
        ],
        "security": [
          {
            "apiKey": []
          }
        ]
      },
      "get": {
        "operationId": "LookupController_listLookups",
        "summary": "List your lookups",
        "description": "Most recent first. Scoped to the account the API key belongs to.",
        "parameters": [
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Default 20, max 100.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "description": "Default 0.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The lookups this account has run.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LookupHistoryEntryDto"
                  }
                }
              }
            }
          },
          "429": {
            "description": "Read rate limit exceeded (20/min).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorDto"
                }
              }
            }
          }
        },
        "tags": [
          "Lookup"
        ],
        "security": [
          {
            "apiKey": []
          }
        ]
      }
    },
    "/v1/lookup/stream": {
      "get": {
        "operationId": "LookupController_streamLookup",
        "summary": "Stream a lookup",
        "description": "Server-sent events. Each platform result is pushed as it resolves, so the first results arrive in well under a second rather than after the full run. Event types: `platform_result`, `breach_result`, `web_mentions_result`, `phone_details_result`, `done`.",
        "parameters": [
          {
            "name": "query",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "email",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "phone",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "tags": [
          "Lookup"
        ],
        "security": [
          {
            "apiKey": []
          }
        ]
      }
    },
    "/v1/lookup/{id}": {
      "get": {
        "operationId": "LookupController_getLookup",
        "summary": "Get one lookup",
        "description": "Returns the full result, or `status: \"processing\"` if the job has not finished yet. A lookup belonging to another account returns 404, identically to an id that does not exist.",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LookupResponseDto"
                }
              }
            }
          },
          "404": {
            "description": "No lookup with that id on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiErrorDto"
                }
              }
            }
          }
        },
        "tags": [
          "Lookup"
        ],
        "security": [
          {
            "apiKey": []
          }
        ]
      }
    }
  },
  "info": {
    "title": "Digital Footprint API",
    "description": "Given an email address, phone number, or username, find where that identity is registered across the internet, plus breach history and public web associations.\n\nEvery platform result is one of three states: registered, not registered, or omitted. A platform we could not get a confident answer for is left out of the response entirely - it is never reported as \"not registered\". Likewise `breaches` and `webMentions` are null when a source is not configured, which is different from an empty array meaning we checked and found nothing.",
    "version": "1.0",
    "contact": {}
  },
  "tags": [
    {
      "name": "Lookup",
      "description": "Identity lookups across 800+ platforms"
    }
  ],
  "servers": [
    {
      "url": "http://localhost:4000"
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "scheme": "bearer",
        "bearerFormat": "dfp_...",
        "type": "http",
        "description": "Your API key."
      }
    },
    "schemas": {
      "PlatformResultDto": {
        "type": "object",
        "properties": {
          "platform": {
            "type": "string",
            "example": "github",
            "description": "Platform identifier."
          },
          "registered": {
            "type": "boolean",
            "example": true,
            "description": "Whether an account exists. A platform we could not confirm is omitted from `results` entirely rather than returned as false."
          },
          "method": {
            "type": "string",
            "example": "public_api",
            "enum": [
              "public_api",
              "account_enumeration",
              "breach_record",
              "domain_derived"
            ],
            "description": "How this was established."
          },
          "icon": {
            "type": "string",
            "example": "https://cdn.jsdelivr.net/npm/simple-icons@latest/icons/github.svg",
            "nullable": true
          },
          "checkedAt": {
            "type": "string",
            "example": "2026-08-26T11:40:55.902Z",
            "description": "When this fact was actually established - not when you asked. Cached results keep their original timestamp."
          },
          "mailboxProvider": {
            "type": "boolean",
            "example": true,
            "description": "Present when this account came with the email address rather than being signed up for (the mailbox provider). Excluded from socialMatches."
          }
        },
        "required": [
          "platform",
          "registered",
          "method",
          "icon",
          "checkedAt"
        ]
      },
      "BreachRecordDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "example": "Adobe"
          },
          "date": {
            "type": "string",
            "example": "2013-10-04",
            "description": "ISO date the breach occurred."
          }
        },
        "required": [
          "name",
          "date"
        ]
      },
      "WebMentionDto": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "example": "Someone (@someone) on a public profile"
          },
          "url": {
            "type": "string",
            "example": "https://example.com/profile"
          },
          "snippet": {
            "type": "string",
            "example": "Public page referencing the query."
          },
          "matchConfidence": {
            "type": "string",
            "example": "confirmed",
            "enum": [
              "confirmed",
              "unconfirmed"
            ],
            "description": "`confirmed` means the exact query string appears on the page."
          }
        },
        "required": [
          "title",
          "url",
          "snippet",
          "matchConfidence"
        ]
      },
      "PhoneDetailsDto": {
        "type": "object",
        "properties": {
          "valid": {
            "type": "boolean",
            "example": true
          },
          "carrier": {
            "type": "string",
            "example": "AT&T",
            "nullable": true
          },
          "lineType": {
            "type": "string",
            "example": "mobile",
            "nullable": true
          },
          "countryCode": {
            "type": "string",
            "example": "US",
            "nullable": true
          }
        },
        "required": [
          "valid",
          "carrier",
          "lineType",
          "countryCode"
        ]
      },
      "LookupResponseDto": {
        "type": "object",
        "properties": {
          "lookupId": {
            "type": "string",
            "example": "8f2c1d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
          },
          "query": {
            "type": "string",
            "example": "someone@example.com"
          },
          "platformsChecked": {
            "type": "number",
            "example": 844,
            "description": "How many platforms were checked."
          },
          "matches": {
            "type": "number",
            "example": 3,
            "description": "Every platform that came back registered."
          },
          "socialMatches": {
            "type": "number",
            "example": 2,
            "description": "`matches` minus accounts that came with the email address (see mailboxProvider). Usually the number you want for risk scoring."
          },
          "servedFromCache": {
            "type": "boolean",
            "example": false,
            "description": "True if any result was reused from cache."
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlatformResultDto"
            }
          },
          "breaches": {
            "nullable": true,
            "description": "null means not checked. An empty array means checked and nothing found.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BreachRecordDto"
            }
          },
          "webMentions": {
            "nullable": true,
            "description": "null means not checked.",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebMentionDto"
            }
          },
          "phoneDetails": {
            "nullable": true,
            "description": "Phone queries only.",
            "allOf": [
              {
                "$ref": "#/components/schemas/PhoneDetailsDto"
              }
            ]
          }
        },
        "required": [
          "lookupId",
          "query",
          "platformsChecked",
          "matches",
          "socialMatches",
          "servedFromCache",
          "results",
          "breaches",
          "webMentions",
          "phoneDetails"
        ]
      },
      "PendingLookupDto": {
        "type": "object",
        "properties": {
          "lookupId": {
            "type": "string",
            "example": "8f2c1d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
          },
          "status": {
            "type": "string",
            "example": "processing",
            "enum": [
              "processing"
            ]
          }
        },
        "required": [
          "lookupId",
          "status"
        ]
      },
      "LookupHistoryEntryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "8f2c1d3e-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
          },
          "query": {
            "type": "string",
            "example": "someone@example.com"
          },
          "queryType": {
            "type": "string",
            "example": "email",
            "enum": [
              "email",
              "phone",
              "username"
            ]
          },
          "platformsChecked": {
            "type": "number",
            "example": 844
          },
          "matches": {
            "type": "number",
            "example": 3
          },
          "socialMatches": {
            "type": "number",
            "example": 2
          },
          "createdAt": {
            "type": "string",
            "example": "2026-08-26T11:40:55.902Z"
          }
        },
        "required": [
          "id",
          "query",
          "queryType",
          "platformsChecked",
          "matches",
          "socialMatches",
          "createdAt"
        ]
      },
      "ApiErrorDto": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "example": {
              "code": "quota_exceeded",
              "message": "You have used all 15 free lookups."
            },
            "description": "Match on `code`, never on `message` - messages get reworded, codes do not."
          }
        },
        "required": [
          "error"
        ]
      },
      "LookupRequestDto": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "An email address, phone number, or username - auto-detected. This is the field to use if you are looking someone up by username, since there is no separate `username` field.",
            "example": "someone@example.com"
          },
          "email": {
            "type": "string",
            "description": "An email address, when you already know it is one. Skips auto-detection.",
            "example": "someone@example.com",
            "format": "email"
          },
          "phone": {
            "type": "string",
            "description": "A phone number in E.164 format, when you already know it is one.",
            "example": "+14155552671"
          }
        }
      }
    }
  }
}