{
  "openapi": "3.1.0",
  "info": {
    "title": "Hometown Workforce Estimate API",
    "version": "2026-10-05",
    "description": "Estimates for home-service jobs: send what the customer wrote, the conversation so far, or photos, and get the hours, the rate, a step-by-step breakdown and a price range, priced by the same engine as Hometown Workforce's own chat.\n\nAuthenticate with `Authorization: Bearer hw_live_…` (or `hw_test_…`). A test key never spends credits. Text that starts with `sandbox:` followed by an entry name answers from a fixed example without calling the model; the entries are `priced`, `needs_detail`, `unsupported`, `unclear`, `rate_limited`, `insufficient_credits`, `content_rejected`, `timeout`, `async` and `failed`. Any other text with a test key is estimated for real, up to 200 times a day. Creating an estimate or a follow-up needs an `Idempotency-Key`. With `mode: async` an estimate answers 202 at once and finishes in the background. Subscribe a webhook endpoint to estimate.completed and estimate.failed to hear when it finishes; every event is signed in the `Hometown-Signature` header. With the Records add-on, keep your end customers in `/v1/customers` and any JSON document in `/v1/records`. Errors are `application/problem+json` with a stable `code`."
  },
  "servers": [
    {
      "url": "https://api.wfskills.com/v1"
    }
  ],
  "tags": [
    {
      "name": "Estimates",
      "description": "Price a job from a description, a conversation or photos, read it back, answer its question, or delete it."
    },
    {
      "name": "Tasks",
      "description": "The jobs Hometown prices in a region, with their unit."
    },
    {
      "name": "Regions",
      "description": "The regions Hometown prices, with their currency, language and time zone."
    },
    {
      "name": "Usage",
      "description": "Requests, errors, credits and latency of the account, by day or by route."
    },
    {
      "name": "Balance",
      "description": "The credits the account has left, by lot."
    },
    {
      "name": "Account",
      "description": "The account and the key that make the call."
    },
    {
      "name": "Webhooks",
      "description": "Endpoints that receive signed events, how to test them and rotate their secret."
    },
    {
      "name": "Events",
      "description": "What happened in the account in the last 30 days, each delivery attempt, and sending an event again."
    },
    {
      "name": "Customers",
      "description": "The end customers you keep with the Records add-on, and the estimates linked to each."
    },
    {
      "name": "Records",
      "description": "Collections of JSON documents you keep with the Records add-on, named by your own ids."
    },
    {
      "name": "Billing",
      "description": "Buying credits and plans with a live key. The dashboard does the same without code."
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "paths": {
    "/": {
      "get": {
        "operationId": "get-api-root",
        "tags": [
          "Account"
        ],
        "summary": "Where the documentation and the contract live",
        "description": "Answers without a key. Use it to check that the API is reachable and which version is current.",
        "security": [],
        "responses": {
          "200": {
            "description": "The current version and two links.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "object",
                    "current_version",
                    "documentation",
                    "contract"
                  ],
                  "properties": {
                    "object": {
                      "type": "string",
                      "enum": [
                        "api"
                      ]
                    },
                    "current_version": {
                      "type": "string",
                      "format": "date"
                    },
                    "documentation": {
                      "type": "string",
                      "format": "uri"
                    },
                    "contract": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                },
                "example": {
                  "object": "api",
                  "current_version": "2026-10-05",
                  "documentation": "https://developers.wfskills.com",
                  "contract": "https://api.wfskills.com/openapi/v1.json"
                }
              }
            }
          }
        }
      }
    },
    "/estimates": {
      "post": {
        "operationId": "createEstimate",
        "summary": "Estimate a job",
        "description": "10 credits for text or a conversation, 25 with photos. A priced, needs_detail, unsupported or unclear answer spends them; any 4xx or 5xx does not. A test key never spends credits; input.text starting with `sandbox:` answers from the sandbox.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          },
          {
            "$ref": "#/components/parameters/Version"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateEstimate"
              },
              "example": {
                "input": {
                  "text": "Install 100 linear feet of wood privacy fence, 6 ft tall"
                },
                "region": "US-MS",
                "metadata": {
                  "order_ref": "A-1042"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The estimate.",
            "headers": {
              "X-Credits-Used": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Credits-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                },
                "example": {
                  "id": "est_01J9Z6X1M4QK2V7H8T3R5N0BCD",
                  "object": "estimate",
                  "environment": "test",
                  "status": "priced",
                  "region": "US-MS",
                  "currency": "USD",
                  "locale": "en-US",
                  "locale_fallback": null,
                  "task": {
                    "slug": "home-improvement.fence.install",
                    "trade": "fence",
                    "action": "install",
                    "unit": "linear_foot",
                    "exact_match": true
                  },
                  "scope": {
                    "quantity": 100,
                    "crew_size": 2,
                    "confidence": "high"
                  },
                  "labor": {
                    "hours": 18,
                    "elapsed_hours": 9.45,
                    "hourly_rate_cents": 7200,
                    "rate_source": "typical",
                    "by_the_hour": false
                  },
                  "price": {
                    "min_cents": 103680,
                    "max_cents": 155520
                  },
                  "calendar": {
                    "days": 2,
                    "minutes_per_day": 480,
                    "total_minutes": 567
                  },
                  "steps": [
                    {
                      "text": "Scope: 100 linear feet of fence (install)"
                    },
                    {
                      "text": "Hours: 4.00 setup + (100 x 0.1400 per linear foot) = 18.00 labor hours"
                    },
                    {
                      "text": "Rate: $72.00/hr, going rate for fence work, no rate on file"
                    },
                    {
                      "text": "Labor: 18.00 h x $72.00 = $1,296.00"
                    },
                    {
                      "text": "Crew of 2: same 18.0 labor hours and the same cost, finished in about 9.4 hours on site"
                    }
                  ],
                  "extras": [],
                  "unpriced": [],
                  "question": null,
                  "usage": {
                    "model_calls": 0,
                    "credits": 0
                  },
                  "metadata": {},
                  "created_at": "2026-10-09T15:04:05+00:00"
                }
              }
            }
          },
          "202": {
            "description": "Accepted, with mode async: the estimate is processing. It finishes in the background, usually within a minute.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-Credits-Held": {
                "$ref": "#/components/headers/X-Credits-Held"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                },
                "example": {
                  "id": "est_01J9Z6X1M4QK2V7H8T3R5N0BCD",
                  "object": "estimate",
                  "status": "processing",
                  "environment": "live",
                  "region": "US-MS",
                  "currency": "USD",
                  "locale": "en-US",
                  "locale_fallback": null,
                  "task": null,
                  "scope": null,
                  "labor": null,
                  "price": null,
                  "calendar": null,
                  "steps": [],
                  "extras": [],
                  "unpriced": [],
                  "question": null,
                  "usage": {
                    "model_calls": 0,
                    "credits": 0
                  },
                  "metadata": {},
                  "created_at": "2026-10-09T15:04:05+00:00"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "402": {
            "$ref": "#/components/responses/Problem"
          },
          "409": {
            "$ref": "#/components/responses/Problem"
          },
          "413": {
            "$ref": "#/components/responses/Problem"
          },
          "415": {
            "$ref": "#/components/responses/Problem"
          },
          "422": {
            "$ref": "#/components/responses/Problem"
          },
          "503": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Estimates"
        ]
      },
      "get": {
        "operationId": "listEstimates",
        "summary": "List estimates, newest first",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The id of the last estimate of the previous page."
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "priced",
                "needs_detail",
                "unsupported",
                "unclear",
                "processing",
                "failed"
              ]
            }
          },
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of estimates.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EstimateList"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Estimates"
        ]
      }
    },
    "/estimates/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "est_01J8Z3Y6Q4…"
          }
        }
      ],
      "get": {
        "operationId": "getEstimate",
        "summary": "Get an estimate",
        "responses": {
          "200": {
            "description": "The estimate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Estimates"
        ]
      },
      "delete": {
        "operationId": "deleteEstimate",
        "summary": "Delete an estimate",
        "description": "Deletes the estimate, its follow-ups and everything they held, at once. A follow-up carries the text of the estimate it answers, so it is deleted with it. Deleting a follow-up leaves the estimate it answered.",
        "responses": {
          "204": {
            "description": "Deleted.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Estimates"
        ]
      }
    },
    "/estimates/{id}/follow-up": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "operationId": "followUpEstimate",
        "summary": "Answer an estimate's question and estimate again",
        "description": "5 credits. The earlier exchange is carried over; send only what the customer says now.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FollowUp"
              },
              "example": {
                "input": {
                  "messages": [
                    {
                      "role": "customer",
                      "content": "About 100 feet"
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new estimate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-Credits-Used": {
                "$ref": "#/components/headers/X-Credits-Used"
              },
              "X-Credits-Remaining": {
                "$ref": "#/components/headers/X-Credits-Remaining"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              }
            }
          },
          "202": {
            "description": "Accepted, with mode async: the estimate is processing. It finishes in the background, usually within a minute.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-Credits-Held": {
                "$ref": "#/components/headers/X-Credits-Held"
              },
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Estimate"
                },
                "example": {
                  "id": "est_01J9Z6X1M4QK2V7H8T3R5N0BCD",
                  "object": "estimate",
                  "status": "processing",
                  "environment": "live",
                  "region": "US-MS",
                  "currency": "USD",
                  "locale": "en-US",
                  "locale_fallback": null,
                  "task": null,
                  "scope": null,
                  "labor": null,
                  "price": null,
                  "calendar": null,
                  "steps": [],
                  "extras": [],
                  "unpriced": [],
                  "question": null,
                  "usage": {
                    "model_calls": 0,
                    "credits": 0
                  },
                  "metadata": {},
                  "created_at": "2026-10-09T15:04:05+00:00"
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "422": {
            "$ref": "#/components/responses/Problem"
          },
          "503": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Estimates"
        ]
      }
    },
    "/tasks": {
      "get": {
        "operationId": "listTasks",
        "summary": "The jobs that can be priced in a region",
        "parameters": [
          {
            "name": "region",
            "in": "query",
            "schema": {
              "type": "string",
              "example": "US-MS"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 100
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of tasks.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskList"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "422": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Tasks"
        ]
      }
    },
    "/regions": {
      "get": {
        "operationId": "listRegions",
        "summary": "Regions, their currency, language and time zone",
        "responses": {
          "200": {
            "description": "The regions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegionList"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Regions"
        ]
      }
    },
    "/usage": {
      "get": {
        "operationId": "getUsage",
        "summary": "Requests, errors, credits and p95 latency, by day or route",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "First day, in UTC. Defaults to 29 days before today. The range covers at most 90 days.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Last day, in UTC, on or after from. Defaults to today.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "group_by",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "day",
                "route"
              ],
              "default": "day"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Usage"
        ]
      }
    },
    "/balance": {
      "get": {
        "operationId": "getBalance",
        "summary": "Credits available, by lot",
        "responses": {
          "200": {
            "description": "The balance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Balance"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Balance"
        ]
      }
    },
    "/me": {
      "get": {
        "operationId": "getMe",
        "summary": "The account and key making the call",
        "responses": {
          "200": {
            "description": "The account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Account"
        ]
      }
    },
    "/webhook_endpoints": {
      "get": {
        "operationId": "listWebhookEndpoints",
        "summary": "List webhook endpoints",
        "description": "The endpoints of the key's environment, newest first.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The id of the last item of the previous page."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of endpoints.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpointList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Webhooks"
        ]
      },
      "post": {
        "operationId": "createWebhookEndpoint",
        "summary": "Create a webhook endpoint",
        "description": "Hometown checks the URL before it is saved. It must use HTTPS on port 443 or 1024 to 65535, resolve to public addresses only and answer without a redirect. An environment has at most 16 active endpoints. The answer carries the signing secret, which is not shown again. This operation takes no Idempotency-Key, so the secret is never stored for a replay.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookEndpoint"
              },
              "example": {
                "url": "https://example.com/hooks/hometown",
                "description": "production",
                "events": [
                  "estimate.completed",
                  "estimate.failed",
                  "credits.low"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The endpoint, with its secret.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "422": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/webhook_endpoints/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "whe_01J9Z6X1M4…"
          }
        }
      ],
      "get": {
        "operationId": "getWebhookEndpoint",
        "summary": "Get a webhook endpoint",
        "responses": {
          "200": {
            "description": "The endpoint.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Webhooks"
        ]
      },
      "patch": {
        "operationId": "updateWebhookEndpoint",
        "summary": "Update a webhook endpoint",
        "description": "Change the URL, the description or the events, or enable or disable the endpoint. A new URL is checked like on create.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateWebhookEndpoint"
              },
              "example": {
                "status": "active"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The endpoint.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "422": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Webhooks"
        ]
      },
      "delete": {
        "operationId": "deleteWebhookEndpoint",
        "summary": "Delete a webhook endpoint",
        "description": "Deliveries still waiting for it are dropped, and its delivery history goes with it.",
        "responses": {
          "204": {
            "description": "Deleted.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/webhook_endpoints/{id}/rotate_secret": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "whe_01J9Z6X1M4…"
          }
        }
      ],
      "post": {
        "operationId": "rotateWebhookSecret",
        "summary": "Rotate the signing secret",
        "description": "A new secret signs every delivery from now on. For 24 hours the previous secret signs too, as a second v1 in Hometown-Signature, so receivers can switch without missing an event.",
        "responses": {
          "200": {
            "description": "The endpoint, with its new secret.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEndpoint"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/webhook_endpoints/{id}/test": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "whe_01J9Z6X1M4…"
          }
        }
      ],
      "post": {
        "operationId": "testWebhookEndpoint",
        "summary": "Send a test event",
        "description": "Sends webhook.test to this endpoint, signed like any event, whatever events it subscribes to and even while it is disabled. It is tried once and never retried; GET the event to read the answer.",
        "responses": {
          "202": {
            "description": "The test event, queued.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Event"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/events": {
      "get": {
        "operationId": "listEvents",
        "summary": "List events, newest first",
        "description": "Events of the key's environment from the last 30 days.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The id of the last item of the previous page."
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/EventType"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of events.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Events"
        ]
      }
    },
    "/events/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "evt_01J9Z6X1M4…"
          }
        }
      ],
      "get": {
        "operationId": "getEvent",
        "summary": "Get an event and its deliveries",
        "description": "The event as receivers got it, and every delivery attempt with what the endpoint answered.",
        "responses": {
          "200": {
            "description": "The event.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventWithDeliveries"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Events"
        ]
      }
    },
    "/events/{id}/redeliver": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "evt_01J9Z6X1M4…"
          }
        }
      ],
      "post": {
        "operationId": "redeliverEvent",
        "summary": "Send an event again",
        "description": "Sends the event again with the same body and Hometown-Event-Id. By default it goes to every active endpoint that listens to its type. With endpoint, it goes only to that endpoint, if it is active. An event older than 30 days answers 422 event_not_redeliverable, or 404 not_found once it has been deleted.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "endpoint": {
                    "type": "string",
                    "description": "A whe_ id: send to this endpoint only."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Queued.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Redelivery"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "422": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Events"
        ]
      }
    },
    "/customers": {
      "get": {
        "operationId": "listCustomers",
        "summary": "List customers",
        "description": "The customers of the key's environment, newest first. Needs the Records add-on and the `customers:write` scope.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The id of the last item of the previous page."
          },
          {
            "name": "external_id",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Only the customer with this external_id."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of customers.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Customers"
        ]
      },
      "post": {
        "operationId": "createCustomer",
        "summary": "Create a customer",
        "description": "external_id is your own id for the customer and is unique in each environment. Phone and email are stored encrypted. Customers and records share a cap of 50,000 per environment. Needs the Records add-on and the `customers:write` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCustomer"
              },
              "example": {
                "external_id": "crm-2291",
                "name": "Dana Reyes",
                "phone": "+16015550123",
                "email": "dana@example.com",
                "region_code": "US-MS"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The customer.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "409": {
            "$ref": "#/components/responses/Problem"
          },
          "422": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Customers"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/customers/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "cus_01J9Z6X1M4QK2V7H8T3R5N0BCD"
          }
        }
      ],
      "get": {
        "operationId": "getCustomer",
        "summary": "Get a customer",
        "description": "Needs the Records add-on and the `customers:write` scope.",
        "responses": {
          "200": {
            "description": "The customer.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Customers"
        ]
      },
      "patch": {
        "operationId": "updateCustomer",
        "summary": "Update a customer",
        "description": "Send only the fields to change; null clears a field. Needs the Records add-on and the `customers:write` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCustomer"
              },
              "example": {
                "phone": "+16015550188"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The customer.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "422": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Customers"
        ]
      },
      "delete": {
        "operationId": "deleteCustomer",
        "summary": "Delete a customer",
        "description": "The customer is erased at once with its phone and email. Estimates linked to it stay and lose the link. Needs the Records add-on and the `customers:write` scope.",
        "responses": {
          "204": {
            "description": "Deleted.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/{id}/estimates": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "example": "cus_01J9Z6X1M4QK2V7H8T3R5N0BCD"
          }
        }
      ],
      "get": {
        "operationId": "listCustomerEstimates",
        "summary": "List a customer's estimates",
        "description": "Estimates created with this customer_id, and their follow-ups, newest first. Needs the Records add-on and the `customers:write` scope.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The id of the last item of the previous page."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of estimates.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EstimateList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Customers"
        ]
      }
    },
    "/records/{collection}": {
      "parameters": [
        {
          "name": "collection",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]{0,39}$",
            "example": "jobs"
          },
          "description": "Lowercase letters, digits and _, starting with a letter. A collection exists while it holds a record."
        }
      ],
      "get": {
        "operationId": "listRecords",
        "summary": "List records",
        "description": "The records of a collection in the key's environment, by id in ascending order. Needs the Records add-on and the `records:read` scope.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "starting_after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The id of the last record of the previous page."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of records.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecordList"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Records"
        ]
      }
    },
    "/records/{collection}/{id}": {
      "parameters": [
        {
          "name": "collection",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]{0,39}$",
            "example": "jobs"
          },
          "description": "Lowercase letters, digits and _, starting with a letter. A collection exists while it holds a record."
        },
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^[A-Za-z0-9._:@-]{1,120}$",
            "example": "job-1042"
          },
          "description": "Your own id for the record."
        }
      ],
      "get": {
        "operationId": "getRecord",
        "summary": "Get a record",
        "description": "Needs the Records add-on and the `records:read` scope.",
        "responses": {
          "200": {
            "description": "The record.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Record"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Records"
        ]
      },
      "put": {
        "operationId": "putRecord",
        "summary": "Create or replace a record",
        "description": "Creates the record, or replaces its payload when updated_at_ms is equal to or later than the stored one. An older write changes nothing and answers with the stored record. The payload is a JSON object of at most 64 KB. Needs the Records add-on and the `records:write` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PutRecord"
              },
              "example": {
                "payload": {
                  "title": "Back fence",
                  "status": "scheduled"
                },
                "updated_at_ms": 1760100000000
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored record, unchanged when the write was older.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Record"
                }
              }
            }
          },
          "201": {
            "description": "The record, created.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Record"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "413": {
            "$ref": "#/components/responses/Problem"
          },
          "422": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Records"
        ]
      },
      "delete": {
        "operationId": "deleteRecord",
        "summary": "Delete a record",
        "description": "The record is erased at once. Needs the Records add-on and the `records:write` scope.",
        "responses": {
          "204": {
            "description": "Deleted.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Records"
        ]
      }
    },
    "/billing/checkout": {
      "post": {
        "operationId": "createCheckoutSession",
        "summary": "Open a Checkout page",
        "description": "Opens a Stripe Checkout page to buy a credit pack, the Business plan or the Records add-on. Send the customer to url; credits arrive when the invoice is paid, a few seconds after the payment, never when the page returns. Needs a live key with the `billing:manage` scope and a billing profile with the legal name, address and country, which the dashboard keeps.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCheckoutSession"
              },
              "example": {
                "pack": "credits_10000",
                "success_url": "https://example.com/billing/done",
                "cancel_url": "https://example.com/billing"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The Checkout page.",
            "headers": {
              "X-Request-Id": {
                "$ref": "#/components/headers/X-Request-Id"
              },
              "Hometown-Version": {
                "$ref": "#/components/headers/Hometown-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckoutSession"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "401": {
            "$ref": "#/components/responses/Problem"
          },
          "402": {
            "$ref": "#/components/responses/Problem"
          },
          "403": {
            "$ref": "#/components/responses/Problem"
          },
          "409": {
            "$ref": "#/components/responses/Problem"
          },
          "422": {
            "$ref": "#/components/responses/Problem"
          },
          "429": {
            "$ref": "#/components/responses/Problem"
          },
          "500": {
            "$ref": "#/components/responses/Problem"
          }
        },
        "tags": [
          "Billing"
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "hw_live_… or hw_test_…"
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64
        },
        "description": "A UUID works. The same key with the same body returns the first answer (with Idempotent-Replayed: true) and is charged once, for 24 hours."
      },
      "Version": {
        "name": "Hometown-Version",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "example": "2026-10-05"
        },
        "description": "The version your code was written for. Every answer carries the version it was given in."
      }
    },
    "responses": {
      "Problem": {
        "description": "An error, as RFC 9457 problem details. `code` says which.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        },
        "headers": {
          "X-Request-Id": {
            "$ref": "#/components/headers/X-Request-Id"
          },
          "Hometown-Version": {
            "$ref": "#/components/headers/Hometown-Version"
          }
        }
      }
    },
    "schemas": {
      "ErrorCode": {
        "type": "string",
        "description": "Codes are added and never renamed or removed; treat a code you do not know by its HTTP status.\n- `api_key_expired` (401): API key expired\n- `api_key_invalid` (401): API key invalid\n- `api_key_missing` (401): API key missing\n- `api_key_paused` (403): API key paused\n- `api_key_revoked` (401): API key revoked\n- `billing_profile_incomplete` (422): Billing profile incomplete\n- `content_rejected` (422): Content rejected\n- `daily_quota_reached` (429): Daily quota reached\n- `event_not_redeliverable` (422): Event not redeliverable\n- `idempotency_conflict` (409): Idempotency conflict\n- `idempotency_key_reused` (422): Idempotency key reused\n- `insufficient_credits` (402): Insufficient credits\n- `internal_error` (500): Internal error\n- `invalid_request` (400): Invalid request\n- `ip_not_allowed` (403): IP address not allowed\n- `method_not_allowed` (405): Method not allowed\n- `model_unavailable` (503): Estimator unavailable\n- `monthly_cap_reached` (402): Monthly cap reached\n- `not_found` (404): Not found\n- `pack_unknown` (422): Unknown pack\n- `payload_too_large` (413): Payload too large\n- `rate_limited` (429): Rate limited\n- `records_cap_reached` (422): Records cap reached\n- `sandbox_daily_cap_reached` (429): Sandbox daily cap reached\n- `sandbox_input_on_live` (422): Sandbox input on a live key\n- `scope_missing` (403): Scope missing\n- `service_unavailable` (503): Service unavailable\n- `tenant_suspended` (403): Account suspended\n- `terms_not_accepted` (403): Terms not accepted\n- `test_key_on_live_route` (403): Live key required\n- `unsupported_in_region` (422): Unsupported in region\n- `unsupported_media_type` (415): Unsupported media type\n- `validation_failed` (422): Validation failed\n- `webhook_endpoint_limit` (422): Webhook endpoint limit reached\n- `webhook_url_rejected` (422): Webhook URL rejected",
        "x-extensible-enum": [
          "api_key_expired",
          "api_key_invalid",
          "api_key_missing",
          "api_key_paused",
          "api_key_revoked",
          "billing_profile_incomplete",
          "content_rejected",
          "daily_quota_reached",
          "event_not_redeliverable",
          "idempotency_conflict",
          "idempotency_key_reused",
          "insufficient_credits",
          "internal_error",
          "invalid_request",
          "ip_not_allowed",
          "method_not_allowed",
          "model_unavailable",
          "monthly_cap_reached",
          "not_found",
          "pack_unknown",
          "payload_too_large",
          "rate_limited",
          "records_cap_reached",
          "sandbox_daily_cap_reached",
          "sandbox_input_on_live",
          "scope_missing",
          "service_unavailable",
          "tenant_suspended",
          "terms_not_accepted",
          "test_key_on_live_route",
          "unsupported_in_region",
          "unsupported_media_type",
          "validation_failed",
          "webhook_endpoint_limit",
          "webhook_url_rejected"
        ]
      },
      "Problem": {
        "type": "object",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code",
          "request_id"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "instance": {
            "type": "string"
          },
          "code": {
            "$ref": "#/components/schemas/ErrorCode"
          },
          "param": {
            "type": [
              "string",
              "null"
            ]
          },
          "errors": {
            "type": "array",
            "description": "Only on validation_failed: one entry per field.",
            "items": {
              "type": "object",
              "properties": {
                "param": {
                  "type": "string"
                },
                "code": {
                  "type": "string"
                },
                "detail": {
                  "type": "string"
                }
              }
            }
          },
          "request_id": {
            "type": "string",
            "example": "req_01J8…"
          }
        }
      },
      "Message": {
        "type": "object",
        "required": [
          "role",
          "content"
        ],
        "properties": {
          "role": {
            "type": "string",
            "enum": [
              "customer",
              "assistant"
            ]
          },
          "content": {
            "type": "string",
            "maxLength": 1000
          }
        }
      },
      "Image": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "string",
            "description": "Base64 of a JPEG, PNG or WebP, at most 5 MB."
          },
          "mime": {
            "type": "string"
          }
        }
      },
      "CreateEstimate": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "input"
        ],
        "properties": {
          "input": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "text": {
                "type": "string",
                "maxLength": 4000
              },
              "messages": {
                "type": "array",
                "maxItems": 10,
                "items": {
                  "$ref": "#/components/schemas/Message"
                }
              },
              "images": {
                "type": "array",
                "maxItems": 4,
                "items": {
                  "$ref": "#/components/schemas/Image"
                }
              }
            }
          },
          "region": {
            "type": "string",
            "description": "Defaults to the account's region."
          },
          "currency": {
            "type": "string",
            "description": "Must be the region's currency."
          },
          "locale": {
            "type": "string",
            "example": "en-US"
          },
          "hourly_rate": {
            "type": "number",
            "minimum": 1,
            "maximum": 2000,
            "description": "Prices at this rate (rate_source override)."
          },
          "crew_size": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10
          },
          "include_extras": {
            "type": "boolean",
            "default": true
          },
          "mode": {
            "type": "string",
            "enum": [
              "sync",
              "async"
            ],
            "default": "sync",
            "description": "sync answers with the finished estimate. async answers 202 with status processing at once; GET the estimate, or listen for estimate.completed and estimate.failed."
          },
          "end_user_id": {
            "type": "string",
            "maxLength": 64
          },
          "metadata": {
            "type": "object",
            "maxProperties": 20,
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            },
            "description": "Returned as it was sent; never read by the model."
          },
          "customer_id": {
            "type": "string",
            "maxLength": 40,
            "description": "Links the estimate to a customer of the Records add-on, in the key's environment. Follow-ups keep the link."
          }
        }
      },
      "FollowUp": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "input"
        ],
        "properties": {
          "input": {
            "type": "object",
            "required": [
              "messages"
            ],
            "properties": {
              "messages": {
                "type": "array",
                "minItems": 1,
                "maxItems": 10,
                "items": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          },
          "region": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "hourly_rate": {
            "type": "number"
          },
          "crew_size": {
            "type": "integer"
          },
          "include_extras": {
            "type": "boolean"
          },
          "metadata": {
            "type": "object"
          }
        }
      },
      "EstimateStatus": {
        "type": "string",
        "description": "priced, needs_detail, unsupported and unclear are answers. processing and failed occur only with mode async. New values may be added; treat one you do not know as not finished.",
        "x-extensible-enum": [
          "priced",
          "needs_detail",
          "unsupported",
          "unclear",
          "processing",
          "failed"
        ]
      },
      "Labor": {
        "type": "object",
        "properties": {
          "hours": {
            "type": "number"
          },
          "elapsed_hours": {
            "type": "number"
          },
          "hourly_rate_cents": {
            "type": "integer"
          },
          "rate_source": {
            "type": "string",
            "enum": [
              "override",
              "tenant",
              "typical",
              "band"
            ]
          },
          "by_the_hour": {
            "type": "boolean"
          }
        }
      },
      "Price": {
        "type": "object",
        "properties": {
          "min_cents": {
            "type": "integer"
          },
          "max_cents": {
            "type": "integer"
          }
        }
      },
      "Estimate": {
        "type": "object",
        "required": [
          "id",
          "object",
          "status",
          "environment",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "object": {
            "type": "string",
            "const": "estimate"
          },
          "status": {
            "$ref": "#/components/schemas/EstimateStatus"
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ]
          },
          "region": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "locale_fallback": {
            "type": [
              "string",
              "null"
            ],
            "description": "en when at least one line of steps came back in English. Null when every line is in the locale."
          },
          "task": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "slug": {
                "type": "string"
              },
              "trade": {
                "type": "string"
              },
              "action": {
                "type": "string"
              },
              "unit": {
                "type": "string"
              },
              "exact_match": {
                "type": "boolean"
              }
            }
          },
          "scope": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "quantity": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "crew_size": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "confidence": {
                "type": "string"
              }
            }
          },
          "labor": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Labor"
              },
              {
                "type": "null"
              }
            ]
          },
          "price": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Price"
              },
              {
                "type": "null"
              }
            ]
          },
          "calendar": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "days": {
                "type": "integer"
              },
              "minutes_per_day": {
                "type": "integer"
              },
              "total_minutes": {
                "type": "integer"
              }
            }
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "text": {
                  "type": "string"
                }
              }
            }
          },
          "extras": {
            "type": "array",
            "description": "Other jobs the customer mentioned, priced alike.",
            "items": {
              "type": "object",
              "properties": {
                "task": {
                  "type": "object",
                  "properties": {
                    "slug": {
                      "type": "string"
                    }
                  }
                },
                "labor": {
                  "$ref": "#/components/schemas/Labor"
                },
                "price": {
                  "$ref": "#/components/schemas/Price"
                }
              }
            }
          },
          "unpriced": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "question": {
            "type": [
              "string",
              "null"
            ],
            "description": "On needs_detail and unclear: what to ask the customer."
          },
          "usage": {
            "type": "object",
            "properties": {
              "model_calls": {
                "type": "integer"
              },
              "credits": {
                "type": "integer"
              }
            }
          },
          "customer_id": {
            "type": "string",
            "description": "Present when the estimate is linked to a customer."
          },
          "metadata": {
            "type": "object"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "error": {
            "type": "object",
            "description": "Only when status is failed: why an async estimate did not finish. Nothing is charged.",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "model_unavailable",
                  "unsupported_in_region",
                  "internal_error"
                ]
              },
              "detail": {
                "type": "string"
              }
            },
            "required": [
              "code",
              "detail"
            ]
          }
        }
      },
      "EstimateList": {
        "type": "object",
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Estimate"
            }
          },
          "has_more": {
            "type": "boolean"
          }
        }
      },
      "TaskList": {
        "type": "object",
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "slug": {
                  "type": "string"
                },
                "trade": {
                  "type": "string"
                },
                "action": {
                  "type": "string"
                },
                "unit": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                }
              }
            }
          },
          "has_more": {
            "type": "boolean"
          }
        }
      },
      "RegionList": {
        "type": "object",
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string"
                },
                "country": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "currency": {
                  "type": "string"
                },
                "locale": {
                  "type": "string"
                },
                "timezone": {
                  "type": "string"
                },
                "tasks_priced": {
                  "type": "integer"
                }
              }
            }
          },
          "has_more": {
            "type": "boolean"
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "object": {
            "const": "usage"
          },
          "from": {
            "type": "string",
            "format": "date"
          },
          "to": {
            "type": "string",
            "format": "date"
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "day": {
                  "type": "string"
                },
                "route": {
                  "type": "string"
                },
                "requests": {
                  "type": "integer"
                },
                "errors": {
                  "type": "integer"
                },
                "credits": {
                  "type": "integer"
                },
                "p95_latency_ms": {
                  "type": [
                    "integer",
                    "null"
                  ]
                }
              }
            }
          }
        }
      },
      "Balance": {
        "type": "object",
        "properties": {
          "object": {
            "const": "balance"
          },
          "available": {
            "type": "integer"
          },
          "lots": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "credits_total": {
                  "type": "integer"
                },
                "credits_remaining": {
                  "type": "integer"
                },
                "expires_at": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "source": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Account": {
        "type": "object",
        "properties": {
          "object": {
            "const": "account"
          },
          "tenant": {
            "type": "object"
          },
          "key": {
            "type": "object"
          },
          "api_version": {
            "type": "string"
          },
          "limits": {
            "type": "object"
          }
        }
      },
      "EventType": {
        "type": "string",
        "x-extensible-enum": [
          "estimate.completed",
          "estimate.failed",
          "credits.low",
          "credits.exhausted",
          "api_key.rotated",
          "api_key.expiring",
          "api_key.suspicious_activity",
          "webhook.test"
        ],
        "description": "New types may be added; ignore a type you do not handle."
      },
      "WebhookEndpoint": {
        "type": "object",
        "required": [
          "id",
          "object",
          "url",
          "events",
          "status",
          "environment",
          "api_version",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "whe_01J9Z6X1M4QK2V7H8T3R5N0BCD"
          },
          "object": {
            "type": "string",
            "const": "webhook_endpoint"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EventType"
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ]
          },
          "disabled_reason": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "failed_deliveries",
              "disabled_by_owner",
              null
            ],
            "description": "failed_deliveries when every retry of a delivery failed; disabled_by_owner when you disabled it."
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ]
          },
          "api_version": {
            "type": "string",
            "format": "date",
            "description": "The account's API version when the endpoint was created. Each event's body uses the account's version at the time of the event; read it in the event's api_version."
          },
          "secret": {
            "type": "string",
            "description": "Only in the answer to create and rotate_secret. Store it: it is not shown again.",
            "example": "hwsec_9f2c…"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WebhookEndpointList": {
        "type": "object",
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEndpoint"
            }
          },
          "has_more": {
            "type": "boolean"
          }
        }
      },
      "CreateWebhookEndpoint": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "url",
          "events"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "HTTPS, on a host that resolves to a public address, answering without redirects."
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "events": {
            "type": "array",
            "minItems": 1,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "enum": [
                "estimate.completed",
                "estimate.failed",
                "credits.low",
                "credits.exhausted",
                "api_key.rotated",
                "api_key.expiring",
                "api_key.suspicious_activity"
              ]
            }
          }
        }
      },
      "UpdateWebhookEndpoint": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "events": {
            "type": "array",
            "minItems": 1,
            "uniqueItems": true,
            "items": {
              "type": "string",
              "enum": [
                "estimate.completed",
                "estimate.failed",
                "credits.low",
                "credits.exhausted",
                "api_key.rotated",
                "api_key.expiring",
                "api_key.suspicious_activity"
              ]
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "disabled"
            ],
            "description": "active enables a disabled endpoint again, within the limit of 16 per environment."
          }
        }
      },
      "Event": {
        "type": "object",
        "required": [
          "id",
          "object",
          "type",
          "api_version",
          "environment",
          "created_at",
          "data"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "evt_01J9Z6X1M4QK2V7H8T3R5N0BCD"
          },
          "object": {
            "type": "string",
            "const": "event"
          },
          "type": {
            "$ref": "#/components/schemas/EventType"
          },
          "api_version": {
            "type": "string",
            "format": "date"
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "required": [
              "object"
            ],
            "description": "object is the resource the event is about, as the API returns it: an estimate, a balance, an API key or a webhook endpoint.",
            "properties": {
              "object": {
                "type": "object"
              },
              "previous": {
                "type": "object",
                "description": "Only on api_key.rotated: the key it replaces and when that key stops working."
              },
              "anomaly": {
                "type": "object",
                "description": "Only on api_key.suspicious_activity: the rule that fired, whether the key was paused, and the figures behind it."
              }
            }
          }
        }
      },
      "EventList": {
        "type": "object",
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Event"
            }
          },
          "has_more": {
            "type": "boolean"
          }
        }
      },
      "Delivery": {
        "type": "object",
        "properties": {
          "endpoint": {
            "type": "string"
          },
          "attempt": {
            "type": "integer",
            "minimum": 1
          },
          "status_code": {
            "type": [
              "integer",
              "null"
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ]
          },
          "duration_ms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "next_attempt_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "EventWithDeliveries": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Event"
          },
          {
            "type": "object",
            "properties": {
              "deliveries": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Delivery"
                }
              }
            }
          }
        ]
      },
      "Redelivery": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Event"
          },
          {
            "type": "object",
            "properties": {
              "deliveries_queued": {
                "type": "integer"
              }
            }
          }
        ]
      },
      "CreateCustomer": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "external_id"
        ],
        "properties": {
          "external_id": {
            "type": "string",
            "maxLength": 120,
            "pattern": "^[A-Za-z0-9._:@-]+$",
            "description": "Your id for the customer, unique in each environment."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 160
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\+[1-9]\\d{6,14}$",
            "description": "E.164, like +16015550123."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "maxLength": 254
          },
          "address": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": false,
            "properties": {
              "line1": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "line2": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "city": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "region": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "postal_code": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              }
            }
          },
          "region_code": {
            "type": [
              "string",
              "null"
            ],
            "description": "A region from GET /v1/regions."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000
          },
          "metadata": {
            "type": "object",
            "maxProperties": 20,
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            },
            "description": "Returned as it was sent."
          }
        }
      },
      "UpdateCustomer": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "external_id": {
            "type": "string",
            "maxLength": 120,
            "pattern": "^[A-Za-z0-9._:@-]+$",
            "description": "Your id for the customer, unique in each environment."
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 160
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\+[1-9]\\d{6,14}$",
            "description": "E.164, like +16015550123."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "maxLength": 254
          },
          "address": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": false,
            "properties": {
              "line1": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "line2": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "city": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "region": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "postal_code": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              }
            }
          },
          "region_code": {
            "type": [
              "string",
              "null"
            ],
            "description": "A region from GET /v1/regions."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000
          },
          "metadata": {
            "type": "object",
            "maxProperties": 20,
            "additionalProperties": {
              "type": "string",
              "maxLength": 500
            },
            "description": "Returned as it was sent."
          }
        }
      },
      "Customer": {
        "type": "object",
        "required": [
          "id",
          "object",
          "environment",
          "external_id",
          "estimates_count",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "cus_01J9Z6X1M4QK2V7H8T3R5N0BCD"
          },
          "object": {
            "type": "string",
            "const": "customer"
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ]
          },
          "external_id": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "address": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": false,
            "properties": {
              "line1": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "line2": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "city": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "region": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "postal_code": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              },
              "country": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 160
              }
            }
          },
          "region": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "metadata": {
            "type": "object"
          },
          "estimates_count": {
            "type": "integer",
            "description": "Estimates linked to this customer."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "CustomerList": {
        "type": "object",
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Customer"
            }
          },
          "has_more": {
            "type": "boolean"
          }
        }
      },
      "PutRecord": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "payload",
          "updated_at_ms"
        ],
        "properties": {
          "payload": {
            "type": "object",
            "description": "Any JSON object, at most 64 KB."
          },
          "updated_at_ms": {
            "type": "integer",
            "minimum": 0,
            "description": "When you changed the record, in milliseconds since the Unix epoch, by your clock. The latest write wins."
          }
        }
      },
      "Record": {
        "type": "object",
        "required": [
          "id",
          "object",
          "environment",
          "collection",
          "payload",
          "updated_at_ms",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "job-1042"
          },
          "object": {
            "type": "string",
            "const": "record"
          },
          "environment": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ]
          },
          "collection": {
            "type": "string",
            "example": "jobs"
          },
          "payload": {
            "type": "object"
          },
          "updated_at_ms": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "RecordList": {
        "type": "object",
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Record"
            }
          },
          "has_more": {
            "type": "boolean"
          }
        }
      },
      "CreateCheckoutSession": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "pack",
          "success_url",
          "cancel_url"
        ],
        "properties": {
          "pack": {
            "type": "string",
            "description": "What to buy: `credits_2500`, `credits_10000` or `credits_50000` (a pack that lasts 12 months), `plan_api_business` (the Business plan) or `addon_records` (the Records add-on). Another value answers 422 `pack_unknown`.",
            "x-extensible-enum": [
              "credits_2500",
              "credits_10000",
              "credits_50000",
              "plan_api_business",
              "addon_records"
            ]
          },
          "success_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2000,
            "description": "Where Stripe sends the customer after paying. HTTPS only."
          },
          "cancel_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2000,
            "description": "Where Stripe sends the customer who leaves without paying. HTTPS only."
          }
        }
      },
      "CheckoutSession": {
        "type": "object",
        "required": [
          "id",
          "object",
          "pack",
          "amount_cents",
          "currency",
          "url",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stripe's Checkout Session id.",
            "examples": [
              "cs_live_a1b2c3"
            ]
          },
          "object": {
            "type": "string",
            "const": "checkout_session"
          },
          "pack": {
            "type": "string",
            "description": "What the page sells, as requested."
          },
          "amount_cents": {
            "type": "integer",
            "description": "What the page charges now, in the minor unit of currency, before tax."
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217, uppercase.",
            "examples": [
              "USD"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The Checkout page. Send the customer here."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "After this moment the page no longer accepts a payment."
          }
        }
      }
    },
    "headers": {
      "X-Request-Id": {
        "description": "The id of this request. Quote it when you contact support.",
        "schema": {
          "type": "string"
        }
      },
      "Hometown-Version": {
        "description": "The API version that answered: the one the request named, or the version the account is pinned to.",
        "schema": {
          "type": "string",
          "format": "date"
        }
      },
      "RateLimit-Policy": {
        "description": "The per-minute limit of the account, as limit;w=60.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit": {
        "description": "Requests left in the current minute: limit, remaining and reset in seconds.",
        "schema": {
          "type": "string"
        }
      },
      "X-Credits-Used": {
        "description": "Credits this request spent. Zero for a test key and for a repeated request.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Credits-Remaining": {
        "description": "Credits left after this request. Live keys only.",
        "schema": {
          "type": "integer"
        }
      },
      "Idempotent-Replayed": {
        "description": "Present when the answer is the stored answer of an earlier request with the same Idempotency-Key.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before trying again.",
        "schema": {
          "type": "integer"
        }
      },
      "X-Credits-Held": {
        "description": "Credits held for an async estimate until it finishes; spent if it answers, given back if it fails. Zero for a test key.",
        "schema": {
          "type": "integer"
        }
      }
    }
  },
  "webhooks": {
    "estimate.completed": {
      "post": {
        "operationId": "event_estimate_completed",
        "summary": "estimate.completed",
        "description": "An async estimate finished. data.object is the estimate. Answer 2xx within 10 seconds. Anything else is retried 8 times over about three days.",
        "parameters": [
          {
            "name": "Hometown-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of t.body>. Two v1 values during the 24 hours after a rotation."
          },
          {
            "name": "Hometown-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id. Deduplicate on it: a retry or a redelivery sends the same id."
          },
          {
            "name": "Hometown-Version",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received."
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    },
    "estimate.failed": {
      "post": {
        "operationId": "event_estimate_failed",
        "summary": "estimate.failed",
        "description": "An async estimate could not finish; nothing was charged. data.object is the estimate, with error. Answer 2xx within 10 seconds. Anything else is retried 8 times over about three days.",
        "parameters": [
          {
            "name": "Hometown-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of t.body>. Two v1 values during the 24 hours after a rotation."
          },
          {
            "name": "Hometown-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id. Deduplicate on it: a retry or a redelivery sends the same id."
          },
          {
            "name": "Hometown-Version",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received."
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    },
    "credits.low": {
      "post": {
        "operationId": "event_credits_low",
        "summary": "credits.low",
        "description": "Live credits fell to 20% or less of what your unexpired lots started with. Checked every hour, and sent once until credits are added. data.object is the balance. Answer 2xx within 10 seconds. Anything else is retried 8 times over about three days.",
        "parameters": [
          {
            "name": "Hometown-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of t.body>. Two v1 values during the 24 hours after a rotation."
          },
          {
            "name": "Hometown-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id. Deduplicate on it: a retry or a redelivery sends the same id."
          },
          {
            "name": "Hometown-Version",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received."
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    },
    "credits.exhausted": {
      "post": {
        "operationId": "event_credits_exhausted",
        "summary": "credits.exhausted",
        "description": "Live credits reached zero. data.object is the balance. Answer 2xx within 10 seconds. Anything else is retried 8 times over about three days.",
        "parameters": [
          {
            "name": "Hometown-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of t.body>. Two v1 values during the 24 hours after a rotation."
          },
          {
            "name": "Hometown-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id. Deduplicate on it: a retry or a redelivery sends the same id."
          },
          {
            "name": "Hometown-Version",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received."
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    },
    "api_key.rotated": {
      "post": {
        "operationId": "event_api_key_rotated",
        "summary": "api_key.rotated",
        "description": "A key was rotated. data.object is the new key; data.previous says when the old one stops working. Answer 2xx within 10 seconds. Anything else is retried 8 times over about three days.",
        "parameters": [
          {
            "name": "Hometown-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of t.body>. Two v1 values during the 24 hours after a rotation."
          },
          {
            "name": "Hometown-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id. Deduplicate on it: a retry or a redelivery sends the same id."
          },
          {
            "name": "Hometown-Version",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received."
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    },
    "api_key.expiring": {
      "post": {
        "operationId": "event_api_key_expiring",
        "summary": "api_key.expiring",
        "description": "A key with an expiry date stops working within 14 days. data.object is the key. Answer 2xx within 10 seconds. Anything else is retried 8 times over about three days.",
        "parameters": [
          {
            "name": "Hometown-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of t.body>. Two v1 values during the 24 hours after a rotation."
          },
          {
            "name": "Hometown-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id. Deduplicate on it: a retry or a redelivery sends the same id."
          },
          {
            "name": "Hometown-Version",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received."
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    },
    "api_key.suspicious_activity": {
      "post": {
        "operationId": "event_api_key_suspicious_activity",
        "summary": "api_key.suspicious_activity",
        "description": "A key behaved unlike itself. data.object is the key; data.anomaly says which rule and whether it was paused. Answer 2xx within 10 seconds. Anything else is retried 8 times over about three days.",
        "parameters": [
          {
            "name": "Hometown-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of t.body>. Two v1 values during the 24 hours after a rotation."
          },
          {
            "name": "Hometown-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id. Deduplicate on it: a retry or a redelivery sends the same id."
          },
          {
            "name": "Hometown-Version",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received."
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    },
    "webhook.test": {
      "post": {
        "operationId": "event_webhook_test",
        "summary": "webhook.test",
        "description": "Sent on request to one endpoint, even a disabled one, whatever events it listens to. data.object is the endpoint. It is tried once and never retried.",
        "parameters": [
          {
            "name": "Hometown-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t=<unix seconds>,v1=<hex HMAC-SHA256 of t.body>. Two v1 values during the 24 hours after a rotation."
          },
          {
            "name": "Hometown-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The event id. Deduplicate on it: a retry or a redelivery sends the same id."
          },
          {
            "name": "Hometown-Version",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Event"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Received."
          }
        },
        "tags": [
          "Webhooks"
        ]
      }
    }
  }
}
