{
  "openapi": "3.1.0",
  "info": {
    "title": "Wired Home Pros listing submissions API",
    "version": "1.0.0",
    "summary": "Submit a real security, AV, home theater, smart-home, low-voltage or electrical business for the directory.",
    "description": "For AI agents and integrations. Every submission is verified before anything is published, and fake or unverifiable submissions are rejected and deleted. Rules:\n\n- Submit only real, operating businesses that install security, audio/video, home theater, smart-home or low-voltage systems in U.S. homes, or electricians and electrical contractors.\n- Use public business information only: the business name, website, public phone number, public business email, city, state and ZIP, trades and license numbers. Never submit personal data about employees or customers, and never guess.\n- Give evidence: \u0022sourceUrls\u0022 must link the public pages you used (the state license record, the business\u0027s own website, a public listing). Agents must send at least one; links to this directory don\u0027t count.\n- Say who you are: \u0022submitter.type\u0022 is business_owner, agent_on_behalf_of_owner (an agent the owner asked to do this) or agent_research (an agent that found the business itself), with the agent\u0027s name, its operator and a contact email for a person.\n- Don\u0027t submit a business that is already listed. If it is listed, the business should use \u0022Claim this listing\u0022 on its page.\n- Never include reviews, ratings, testimonials or claims you can\u0027t link to a source; they are ignored and can get a submission rejected.\n- Every submission is verified before anything is published: we check the state license record where the state publishes one and may call the business at a number on public record. Submissions that are fake, closed, outside the trades listed, outside the United States, duplicates or can\u0027t be verified are rejected and deleted. Submitting does not guarantee a listing.\n- New listings start as unclaimed: only businesses that claim their listing or sign up receive quote requests from homeowners.\n- Set \u0022agreeToTerms\u0022 to true to accept the Terms of Service and these rules. Send an Idempotency-Key header so a retry never creates a second submission.\n\nLimits: anonymous callers 3 accepted submissions per hour and 10 per day per IP address; registered agents (API key) more. Bodies up to 65536 bytes. Errors are RFC 7807 problem\u002Bjson with a stable \u0022code\u0022. Human-readable guide: https://wiredhomepros.com/for-ai-agents/",
    "termsOfService": "https://wiredhomepros.com/terms/",
    "contact": {
      "name": "Wired Home Pros",
      "url": "https://wiredhomepros.com/for-ai-agents/",
      "email": "hello@wiredhomepros.com"
    }
  },
  "jsonSchemaDialect": "https://spec.openapis.org/oas/3.1/dialect/base",
  "servers": [
    {
      "url": "https://wiredhomepros.com"
    }
  ],
  "externalDocs": {
    "description": "Rules for AI agents",
    "url": "https://wiredhomepros.com/for-ai-agents/"
  },
  "security": [
    {},
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/api/v1/listings/submissions": {
      "post": {
        "operationId": "submitListing",
        "summary": "Submit a business for the directory",
        "description": "Queues the business for verification. Returns 202 with a submissionId; poll statusUrl for the outcome.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Recommended. Retrying with the same key and body returns the first answer (header Idempotent-Replayed: true); a different body is refused. Remembered for 24 hours.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ListingSubmission"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Received and queued for verification. Not a listing yet.",
            "headers": {
              "Location": {
                "description": "The status URL.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmissionAccepted"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON or Idempotency-Key (code invalid_json, invalid_idempotency_key).",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Unknown or disabled API key (code invalid_api_key). Leave the key out to submit anonymously.",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "413": {
            "description": "Body too large (code payload_too_large).",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "415": {
            "description": "Not JSON (code unsupported_media_type).",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Field errors (code validation_failed, with errors[]) or a reused Idempotency-Key (code idempotency_key_reused).",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Over the limit (code rate_limited).",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "Submissions are paused (code disabled).",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/listings/submissions/{submissionId}": {
      "get": {
        "operationId": "getSubmissionStatus",
        "summary": "Check a submission\u0027s status",
        "description": "The verification status only; it never repeats the submitted details. Poll at most once a minute.",
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "submissionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^sub_[A-Za-z0-9_-]{22}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The current status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmissionStatus"
                }
              }
            }
          },
          "404": {
            "description": "Unknown submission (code not_found).",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many lookups (code rate_limited).",
            "content": {
              "application/problem\u002Bjson": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Optional. Registered agents get higher limits; ask at hello@wiredhomepros.com. \u0022Authorization: Bearer \u003Ckey\u003E\u0022 also works."
      }
    },
    "schemas": {
      "ListingSubmission": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "companyName",
          "phone",
          "address",
          "trades",
          "submitter",
          "agreeToTerms"
        ],
        "properties": {
          "companyName": {
            "type": "string",
            "description": "The business\u0027s name as it appears on its license or website.",
            "maxLength": 120,
            "examples": [
              "Acme Security \u0026 AV"
            ]
          },
          "website": {
            "type": "string",
            "description": "The business\u0027s own website (https:// is added when missing).",
            "maxLength": 200,
            "format": "uri",
            "examples": [
              "https://acmesecurity.example"
            ]
          },
          "phone": {
            "type": "string",
            "description": "The business\u0027s PUBLIC phone number (license record, website or listing). 10-digit U.S. number.",
            "maxLength": 30,
            "examples": [
              "\u002B16195550100"
            ]
          },
          "publicEmail": {
            "type": "string",
            "description": "The business\u0027s public email, when published. Optional.",
            "maxLength": 120,
            "format": "email"
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "trades": {
            "type": "array",
            "minItems": 1,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "enum": [
                "electrical",
                "security",
                "av",
                "home-theater",
                "smart-home",
                "low-voltage"
              ]
            }
          },
          "specialties": {
            "type": "array",
            "maxItems": 20,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "enum": [
                "cameras",
                "alarm-monitoring",
                "access-control",
                "whole-home-audio",
                "tv-mounting",
                "theater-rooms",
                "ev-chargers",
                "panel-upgrades",
                "generators",
                "lighting-control",
                "networking"
              ]
            }
          },
          "serviceRadiusMiles": {
            "type": "integer",
            "minimum": 1,
            "maximum": 500,
            "description": "How far the business travels for jobs."
          },
          "licenseNumbers": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "string",
              "maxLength": 40,
              "pattern": "^[A-Za-z0-9][A-Za-z0-9 .\\-/]*$"
            },
            "description": "State contractor, electrical or low-voltage license numbers."
          },
          "licenseState": {
            "type": "string",
            "description": "Two-letter state of the license numbers, when not the address state.",
            "maxLength": 20,
            "examples": [
              "CA"
            ]
          },
          "yearsInBusiness": {
            "type": "integer",
            "minimum": 0,
            "maximum": 150
          },
          "teamSize": {
            "type": "string",
            "description": "Team size.",
            "enum": [
              "1",
              "2-5",
              "6-15",
              "16-50",
              "50-plus"
            ]
          },
          "biddingSoftware": {
            "type": "string",
            "description": "What the business uses for bidding/estimating (business_owner submissions; optional).",
            "enum": [
              "bidglory",
              "d-tools",
              "jetbuilt",
              "portal-io",
              "si-d-tools",
              "simpro",
              "servicetitan",
              "housecall-pro",
              "jobber",
              "accelo-connectwise-autotask",
              "spreadsheets",
              "pen-and-paper",
              "other"
            ]
          },
          "biddingSoftwareOther": {
            "type": "string",
            "description": "When biddingSoftware is \u0022other\u0022.",
            "maxLength": 100
          },
          "accountingSoftware": {
            "type": "string",
            "description": "What the business uses for accounting (optional).",
            "enum": [
              "quickbooks-online",
              "quickbooks-desktop",
              "xero",
              "sage",
              "erpnext",
              "netsuite",
              "microsoft-dynamics",
              "other",
              "none-accountant"
            ]
          },
          "accountingSoftwareOther": {
            "type": "string",
            "description": "When accountingSoftware is \u0022other\u0022.",
            "maxLength": 100
          },
          "notes": {
            "type": "string",
            "description": "Anything the reviewers should know. Not published. Put links in sourceUrls.",
            "maxLength": 2000
          },
          "submitter": {
            "$ref": "#/components/schemas/Submitter"
          },
          "sourceUrls": {
            "type": "array",
            "maxItems": 10,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "format": "uri",
              "maxLength": 500
            },
            "description": "Public evidence pages you used. Required (at least one) for agent submissions."
          },
          "agreeToTerms": {
            "const": true,
            "description": "Accepts https://wiredhomepros.com/terms/ and the rules at https://wiredhomepros.com/for-ai-agents/."
          }
        }
      },
      "Address": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "city",
          "state",
          "zip"
        ],
        "properties": {
          "street": {
            "type": "string",
            "description": "Optional. Never shown on an unclaimed listing.",
            "maxLength": 150
          },
          "city": {
            "type": "string",
            "description": "City.",
            "maxLength": 80
          },
          "state": {
            "type": "string",
            "description": "Two-letter U.S. state (or its name).",
            "maxLength": 20,
            "examples": [
              "CA"
            ]
          },
          "zip": {
            "type": "string",
            "pattern": "^\\d{5}(-\\d{4})?$"
          }
        }
      },
      "Submitter": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "type",
          "contactEmail"
        ],
        "description": "agentName and agentOperator are required when type is agent_on_behalf_of_owner or agent_research.",
        "properties": {
          "type": {
            "type": "string",
            "description": "Who is submitting.",
            "enum": [
              "business_owner",
              "agent_on_behalf_of_owner",
              "agent_research"
            ]
          },
          "agentName": {
            "type": "string",
            "description": "The agent\u0027s name and version.",
            "maxLength": 100,
            "examples": [
              "ExampleBot 2.1"
            ]
          },
          "agentOperator": {
            "type": "string",
            "description": "The company or person running the agent.",
            "maxLength": 120,
            "examples": [
              "Example AI, Inc."
            ]
          },
          "contactEmail": {
            "type": "string",
            "description": "A person we can email about this submission.",
            "maxLength": 120,
            "format": "email"
          },
          "contactName": {
            "type": "string",
            "description": "That person\u0027s name. Optional.",
            "maxLength": 120
          },
          "role": {
            "type": "string",
            "description": "business_owner only: the person\u0027s role at the business.",
            "enum": [
              "owner",
              "manager",
              "employee",
              "other"
            ]
          }
        }
      },
      "SubmissionAccepted": {
        "type": "object",
        "required": [
          "submissionId",
          "status",
          "statusUrl"
        ],
        "properties": {
          "submissionId": {
            "type": "string",
            "description": "Keep it: it is the only way to look the submission up."
          },
          "status": {
            "const": "received"
          },
          "statusUrl": {
            "type": "string",
            "description": "GET it for the outcome.",
            "format": "uri"
          }
        }
      },
      "SubmissionStatus": {
        "type": "object",
        "required": [
          "submissionId",
          "status",
          "message",
          "receivedAt",
          "updatedAt",
          "statusUrl"
        ],
        "properties": {
          "submissionId": {
            "type": "string",
            "description": "The submission id."
          },
          "status": {
            "type": "string",
            "description": "received, verifying, needs_review (a person is looking), approved (the listing is being added) or rejected (deleted; see reason).",
            "enum": [
              "received",
              "verifying",
              "approved",
              "needs_review",
              "rejected"
            ]
          },
          "message": {
            "type": "string",
            "description": "A short explanation."
          },
          "reason": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Why.",
                "enum": [
                  "unverifiable",
                  "not_a_business",
                  "closed",
                  "out_of_scope",
                  "outside_us",
                  "duplicate",
                  "license_mismatch",
                  "insufficient_evidence",
                  "withdrawn",
                  "not_eligible"
                ]
              },
              "message": {
                "type": "string",
                "description": "The reason in words."
              }
            }
          },
          "receivedAt": {
            "type": "string",
            "description": "When it was received.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "When the status last changed.",
            "format": "date-time"
          },
          "statusUrl": {
            "type": "string",
            "description": "This resource.",
            "format": "uri"
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 7807 problem details.",
        "required": [
          "type",
          "title",
          "status",
          "code"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "Link to the explanation of this error.",
            "format": "uri"
          },
          "title": {
            "type": "string",
            "description": "Short summary."
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string",
            "description": "What went wrong."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code.",
            "enum": [
              "validation_failed",
              "invalid_json",
              "unsupported_media_type",
              "payload_too_large",
              "invalid_api_key",
              "invalid_idempotency_key",
              "idempotency_key_reused",
              "rate_limited",
              "not_found",
              "disabled"
            ]
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FieldError"
            }
          }
        }
      },
      "FieldError": {
        "type": "object",
        "required": [
          "field",
          "pointer",
          "code",
          "message"
        ],
        "properties": {
          "field": {
            "type": "string",
            "description": "Dotted path, e.g. address.zip or sourceUrls[1]."
          },
          "pointer": {
            "type": "string",
            "description": "RFC 6901 JSON Pointer, e.g. /address/zip."
          },
          "code": {
            "type": "string",
            "description": "required, not_allowed, invalid, invalid_url, invalid_email, invalid_phone, invalid_state, invalid_zip, too_long, too_short, too_many, too_many_links, out_of_range, own_site, unknown_field or wrong_type."
          },
          "message": {
            "type": "string",
            "description": "How to fix it."
          }
        }
      }
    }
  }
}