{
  "openapi": "3.1.0",
  "info": {
    "title": "Doorknock",
    "version": "1.0.0",
    "description": "Research and qualify a company from its own domain, and write the result to HubSpot.\n\nEverything returned was read live from the company's own website or its public DNS records, and carries the source that produced it. Nothing is bought from a data provider and nothing is inferred about revenue, headcount or ownership.\n\nThe HubSpot operations need a private app token sent as the X-HubSpot-Token header. It is never a parameter, so it cannot end up in a model's context window.",
    "license": {
      "name": "MIT"
    }
  },
  "servers": [
    {
      "url": "https://doorknock-eight.vercel.app"
    }
  ],
  "paths": {
    "/v1/research": {
      "get": {
        "operationId": "researchCompany",
        "summary": "Read a company home page and its DNS, and report what is actually there.",
        "description": "Returns the marketing and CRM tools loaded on the home page, which standard pages exist, who handles the domain's email, and whether outbound email is protected. Every fact carries the URL or DNS query it came from. Reports nothing about headcount, revenue or ownership, because none of that is knowable from a domain. Detects 34 technologies.",
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The company domain, for example example.com.au."
          }
        ],
        "responses": {
          "200": {
            "description": "The research result."
          }
        }
      }
    },
    "/v1/qualify": {
      "post": {
        "operationId": "qualifyLead",
        "summary": "Research a company and score it against a lead profile you supply.",
        "description": "The profile travels with the call and comes back with the answer, so the rules that produced a verdict are always visible. A rule whose signal could not be determined blocks the tier rather than counting as a miss.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "domain"
                ],
                "properties": {
                  "domain": {
                    "type": "string"
                  },
                  "icp": {
                    "type": "object",
                    "description": "Rules written against the ids returned by /v1/signals.",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "must_have": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/Rule"
                        }
                      },
                      "nice_to_have": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/Rule"
                        }
                      },
                      "disqualify_if": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/Rule"
                        }
                      },
                      "tier_a_score": {
                        "type": "number"
                      },
                      "tier_b_score": {
                        "type": "number"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The research result and the qualification."
          }
        }
      }
    },
    "/v1/email-posture": {
      "get": {
        "operationId": "checkEmailPosture",
        "summary": "MX, SPF and DMARC for a domain, read in plain words.",
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The company domain, for example example.com.au."
          }
        ],
        "responses": {
          "200": {
            "description": "The email posture."
          }
        }
      }
    },
    "/v1/signals": {
      "get": {
        "operationId": "listSignals",
        "summary": "The signal vocabulary that lead profile rules are written against.",
        "responses": {
          "200": {
            "description": "Every signal id, what it means, and what its absence proves."
          }
        }
      }
    },
    "/v1/hubspot-connection": {
      "get": {
        "operationId": "hubspotCheckConnection",
        "summary": "Confirm the HubSpot token works and report the portal and scopes.",
        "security": [
          {
            "hubspotToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal id and scopes."
          },
          "401": {
            "description": "No token on the request."
          }
        }
      }
    },
    "/v1/hubspot-company": {
      "get": {
        "operationId": "hubspotFindCompany",
        "summary": "Find a company in the connected HubSpot portal by exact domain.",
        "security": [
          {
            "hubspotToken": []
          }
        ],
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The company domain, for example example.com.au."
          }
        ],
        "responses": {
          "200": {
            "description": "The company record, or found: false."
          }
        }
      }
    },
    "/v1/hubspot-write": {
      "post": {
        "operationId": "hubspotWriteCompany",
        "summary": "Create or update a company, and optionally add a timeline note.",
        "description": "Properties the portal does not have are skipped and listed back rather than failing the whole write. A write that would change nothing reports unchanged instead of touching the record.",
        "security": [
          {
            "hubspotToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "domain",
                  "properties"
                ],
                "properties": {
                  "domain": {
                    "type": "string"
                  },
                  "properties": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    }
                  },
                  "note": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What was written."
          },
          "401": {
            "description": "No token on the request."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "hubspotToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-HubSpot-Token"
      }
    },
    "schemas": {
      "Rule": {
        "type": "object",
        "required": [
          "signal"
        ],
        "properties": {
          "signal": {
            "type": "string",
            "description": "A signal id from /v1/signals."
          },
          "expect": {
            "type": "string",
            "enum": [
              "yes",
              "no"
            ],
            "default": "yes"
          },
          "why": {
            "type": "string"
          },
          "weight": {
            "type": "number",
            "default": 1
          }
        }
      }
    }
  }
}