{
  "openapi": "3.1.0",
  "info": {
    "title": "Huddlekit API",
    "version": "1.0.0",
    "summary": "Read and create feedback comments, change their status and subscribe to comment webhooks.",
    "description": "The Huddlekit REST API reads a workspace's projects, web apps, documents and comments, creates comments, changes a comment's status and manages webhook subscriptions. It cannot edit a comment's text or delete comments.\n\n**Authentication:** a workspace API key sent as `Authorization: Bearer hk_live_…`. Keys have a `read` scope (GET calls), a `write` scope (POST, PATCH and DELETE calls), or both.\n\n**Plan:** every call except `DELETE /hooks/{id}` requires an active or trialing Team subscription (a past-due payment fails the check); otherwise it returns 403 with `requiredPlans`.\n\n**Rate limits:** counted per API key, per endpoint group and separately for reads and writes: up to 200 reads and 30 writes a minute. Exceeding a limit returns 429 with a `Retry-After` header in seconds. Refused calls count too.\n\n**Errors:** JSON bodies of the form `{\"error\": \"…\", \"detail\": \"…\"}` (`detail` is optional).\n\nGuides: https://huddlekit.com/support/using-the-rest-api (REST API) and https://huddlekit.com/support/setting-up-webhooks (webhooks). Overview: https://huddlekit.com/integrations/api",
    "termsOfService": "https://huddlekit.com/terms",
    "contact": {
      "name": "Huddlekit",
      "email": "hello@huddlekit.com",
      "url": "https://huddlekit.com"
    }
  },
  "externalDocs": {
    "description": "REST API guide",
    "url": "https://huddlekit.com/support/using-the-rest-api"
  },
  "servers": [
    {
      "url": "https://app.huddlekit.com/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "The API key and its workspace."
    },
    {
      "name": "Projects",
      "description": "Website projects, web apps and documents that comments are attached to."
    },
    {
      "name": "Comments",
      "description": "Read, create and change the status of comments."
    },
    {
      "name": "Webhook subscriptions",
      "description": "Subscribe URLs to comment events (REST hooks)."
    },
    {
      "name": "Events",
      "description": "Sample event payloads."
    },
    {
      "name": "Webhook events",
      "description": "Requests Huddlekit sends to subscribed URLs."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getCurrentApiKey",
        "tags": [
          "Account"
        ],
        "summary": "Get the current API key's workspace and scopes",
        "description": "Returns the workspace the API key belongs to, the key's id and scopes, and the `source` value that events caused by this key carry. Use it to test that a key works. Requires the `read` scope.",
        "responses": {
          "200": {
            "description": "The key's workspace and scopes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyInfo"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/projects": {
      "get": {
        "operationId": "listProjects",
        "tags": [
          "Projects"
        ],
        "summary": "List everything comments can be attached to",
        "description": "Lists every website project, web app and document in the workspace. Each item's `id` and `surface` are what `listComments` and `createComment` take as `parent_id` and `surface`. Not paginated. Requires the `read` scope.",
        "responses": {
          "200": {
            "description": "All projects, web apps and documents.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListProjectsResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/comments": {
      "get": {
        "operationId": "listComments",
        "tags": [
          "Comments"
        ],
        "summary": "List comments on a project, web app or document",
        "description": "Returns the newest comments on one parent (a project, web app or document), newest first, up to `limit`. The fields returned depend on `surface`. Requires the `read` scope.",
        "parameters": [
          {
            "name": "parent_id",
            "in": "query",
            "required": true,
            "description": "Id of the project, web app or document (from `listProjects`). Must belong to the key's workspace.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "project_id",
            "in": "query",
            "required": false,
            "deprecated": true,
            "description": "Deprecated alias of `parent_id`, used only when `parent_id` is absent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/Surface"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of comments to return, 1 to 200. Defaults to 50. Out-of-range values are clamped.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Comments on the parent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListCommentsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      },
      "post": {
        "operationId": "createComment",
        "tags": [
          "Comments"
        ],
        "summary": "Create a comment",
        "description": "Adds a comment to a project, web app or document in the key's workspace. The comment is attributed to a guest author named `API`, is not private, and gets the next comment number. It triggers a `comment.created` event with `source` set to `connector:<key_id>`; webhook subscriptions created with the same key do not receive it. Requires the `write` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCommentRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The comment was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateCommentResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/comments/{id}": {
      "patch": {
        "operationId": "updateCommentStatus",
        "tags": [
          "Comments"
        ],
        "summary": "Change a comment's status",
        "description": "Sets the status of one comment. Status is the only field the API can change: comment text cannot be edited (sending `text` returns 400) and comments cannot be deleted. Setting `resolved` marks the comment resolved; any other status marks it unresolved. A real change triggers a `comment.status_changed` event with `source` set to `connector:<key_id>`. Requires the `write` scope.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the comment to update (a UUID).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCommentStatusRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The status was updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdateCommentStatusResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/hooks": {
      "get": {
        "operationId": "listWebhookSubscriptions",
        "tags": [
          "Webhook subscriptions"
        ],
        "summary": "List this key's webhook subscriptions",
        "description": "Lists the webhook subscriptions created with this API key, newest first. Webhooks created in the Huddlekit app or with other keys are not shown. Requires the `read` scope.",
        "responses": {
          "200": {
            "description": "This key's subscriptions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListWebhookSubscriptionsResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      },
      "post": {
        "operationId": "createWebhookSubscription",
        "tags": [
          "Webhook subscriptions"
        ],
        "summary": "Subscribe a URL to webhook events",
        "description": "Subscribes an https:// URL to comment events (REST-hook style, as used by Zapier). Deliveries are signed with HMAC-SHA256 in the `X-Huddlekit-Signature` header; see the `webhooks` section for payloads. Idempotent per key and URL: subscribing the same URL again with the same key reactivates the existing subscription, replaces its event types and returns 200 without a secret. A new subscription returns 201 with its signing secret, shown only this once. The subscription also appears in the workspace's webhook list in the Huddlekit app. Events caused by this same key are never delivered to it. Requires the `write` scope.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookSubscriptionRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "This key already had a subscription for this URL; it was reactivated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscriptionResubscribed"
                }
              }
            }
          },
          "201": {
            "description": "The subscription was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscriptionCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "The key lacks the `write` scope, or the workspace has no active Team subscription (either the API plan check, with `requiredPlans`, or `{\"error\":\"Webhook subscriptions require an active Team plan\"}`).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/PlanRequiredError"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/hooks/{id}": {
      "delete": {
        "operationId": "deleteWebhookSubscription",
        "tags": [
          "Webhook subscriptions"
        ],
        "summary": "Unsubscribe a webhook",
        "description": "Deletes a webhook subscription that was created with this API key. Idempotent: returns 200 whether or not anything was deleted, with `deleted` saying which. It cannot delete webhooks created in the Huddlekit app or with another key. Not plan-gated: unlike every other call, it works even if the workspace is no longer on the Team plan, so automations can always be switched off. Requires the `write` scope.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id of the subscription to delete (a UUID, from `createWebhookSubscription` or `listWebhookSubscriptions`).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done. `deleted` is false if there was nothing to delete.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteWebhookSubscriptionResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenScope"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "description": "The subscription could not be deleted. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Could not unsubscribe. Please retry."
                }
              }
            }
          }
        }
      }
    },
    "/events/recent": {
      "get": {
        "operationId": "listRecentEvents",
        "tags": [
          "Events"
        ],
        "summary": "Get sample webhook payloads from recent comments",
        "description": "Builds sample webhook payloads from the workspace's newest comments, in exactly the shape a live delivery has, so you can map fields before any event fires. These are not replayed deliveries: `event_id` is `sample:<comment id>`, `occurred_at` is the comment's creation time, `source` is `app`, and `changed` is only filled (with a representative status change) when `event` is `comment.status_changed`. Requires the `read` scope.",
        "parameters": [
          {
            "name": "event",
            "in": "query",
            "required": false,
            "description": "Event type to render the samples as. Defaults to `comment.created`.",
            "schema": {
              "$ref": "#/components/schemas/EventType",
              "default": "comment.created"
            }
          },
          {
            "name": "surface",
            "in": "query",
            "required": false,
            "description": "Only use comments from this surface. Omit to use all surfaces.",
            "schema": {
              "$ref": "#/components/schemas/Surface"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Number of samples, 1 to 25. Defaults to 3. Out-of-range values are clamped.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 3
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sample payloads.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListRecentEventsResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    }
  },
  "webhooks": {
    "comment.created": {
      "post": {
        "operationId": "onCommentCreated",
        "tags": [
          "Webhook events"
        ],
        "summary": "A comment was created",
        "description": "Sent when a comment is added. Website and web app comments are held for at least 10 seconds first so the screenshot is usually ready and included; document comments are sent without the hold. Sent as a JSON POST to every active subscription (and every webhook set up in the Huddlekit app) that includes this event type, while the workspace is on the Team plan. Respond with any 2xx within 10 seconds. Any other status (redirects are not followed), an error or a timeout is a failure: the delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours (6 attempts in all) and then set aside, and an endpoint that has failed 20 times in a row over more than 24 hours is switched off. Delivery is at-least-once; de-duplicate on `X-Huddlekit-Event-Id`.",
        "security": [],
        "parameters": [
          {
            "name": "X-Huddlekit-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex>`. `v1` is the HMAC-SHA256 of `<t>.<raw request body>`, keyed with the subscription's full signing secret (including the `whsec_` prefix). During a secret rotation the header carries a second `v1`; accept the request if any `v1` matches. Reject requests whose `t` is more than 5 minutes from your clock.",
            "schema": {
              "type": "string"
            },
            "example": "t=1790496000,v1=5f2b1c0e9a7d3f6b8c4e2a1d0f9b7c5e3a1d8f6b4c2e0a9d7f5b3c1e8a6d4f2b"
          },
          {
            "name": "X-Huddlekit-Event",
            "in": "header",
            "required": true,
            "description": "The event type, the same as `event` in the body.",
            "schema": {
              "$ref": "#/components/schemas/EventType"
            }
          },
          {
            "name": "X-Huddlekit-Event-Id",
            "in": "header",
            "required": true,
            "description": "The event id, the same as `event_id` in the body. Unchanged across retries; use it to de-duplicate.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "description": "Always `Huddlekit-Webhooks/1`.",
            "schema": {
              "type": "string",
              "const": "Huddlekit-Webhooks/1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CommentCreatedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges the delivery. The response body is ignored."
          }
        }
      }
    },
    "comment.status_changed": {
      "post": {
        "operationId": "onCommentStatusChanged",
        "tags": [
          "Webhook events"
        ],
        "summary": "A comment's status changed",
        "description": "Sent when a comment's status changes. `changed.status` has the old and new values. Sent as a JSON POST to every active subscription (and every webhook set up in the Huddlekit app) that includes this event type, while the workspace is on the Team plan. Respond with any 2xx within 10 seconds. Any other status (redirects are not followed), an error or a timeout is a failure: the delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours (6 attempts in all) and then set aside, and an endpoint that has failed 20 times in a row over more than 24 hours is switched off. Delivery is at-least-once; de-duplicate on `X-Huddlekit-Event-Id`.",
        "security": [],
        "parameters": [
          {
            "name": "X-Huddlekit-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex>`. `v1` is the HMAC-SHA256 of `<t>.<raw request body>`, keyed with the subscription's full signing secret (including the `whsec_` prefix). During a secret rotation the header carries a second `v1`; accept the request if any `v1` matches. Reject requests whose `t` is more than 5 minutes from your clock.",
            "schema": {
              "type": "string"
            },
            "example": "t=1790496000,v1=5f2b1c0e9a7d3f6b8c4e2a1d0f9b7c5e3a1d8f6b4c2e0a9d7f5b3c1e8a6d4f2b"
          },
          {
            "name": "X-Huddlekit-Event",
            "in": "header",
            "required": true,
            "description": "The event type, the same as `event` in the body.",
            "schema": {
              "$ref": "#/components/schemas/EventType"
            }
          },
          {
            "name": "X-Huddlekit-Event-Id",
            "in": "header",
            "required": true,
            "description": "The event id, the same as `event_id` in the body. Unchanged across retries; use it to de-duplicate.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "description": "Always `Huddlekit-Webhooks/1`.",
            "schema": {
              "type": "string",
              "const": "Huddlekit-Webhooks/1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CommentStatusChangedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges the delivery. The response body is ignored."
          }
        }
      }
    },
    "comment.text_changed": {
      "post": {
        "operationId": "onCommentTextChanged",
        "tags": [
          "Webhook events"
        ],
        "summary": "A comment's text was edited",
        "description": "Sent when a comment's text is edited. `changed.text.new` has the new text; the previous text is never sent. Sent as a JSON POST to every active subscription (and every webhook set up in the Huddlekit app) that includes this event type, while the workspace is on the Team plan. Respond with any 2xx within 10 seconds. Any other status (redirects are not followed), an error or a timeout is a failure: the delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours (6 attempts in all) and then set aside, and an endpoint that has failed 20 times in a row over more than 24 hours is switched off. Delivery is at-least-once; de-duplicate on `X-Huddlekit-Event-Id`.",
        "security": [],
        "parameters": [
          {
            "name": "X-Huddlekit-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex>`. `v1` is the HMAC-SHA256 of `<t>.<raw request body>`, keyed with the subscription's full signing secret (including the `whsec_` prefix). During a secret rotation the header carries a second `v1`; accept the request if any `v1` matches. Reject requests whose `t` is more than 5 minutes from your clock.",
            "schema": {
              "type": "string"
            },
            "example": "t=1790496000,v1=5f2b1c0e9a7d3f6b8c4e2a1d0f9b7c5e3a1d8f6b4c2e0a9d7f5b3c1e8a6d4f2b"
          },
          {
            "name": "X-Huddlekit-Event",
            "in": "header",
            "required": true,
            "description": "The event type, the same as `event` in the body.",
            "schema": {
              "$ref": "#/components/schemas/EventType"
            }
          },
          {
            "name": "X-Huddlekit-Event-Id",
            "in": "header",
            "required": true,
            "description": "The event id, the same as `event_id` in the body. Unchanged across retries; use it to de-duplicate.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "description": "Always `Huddlekit-Webhooks/1`.",
            "schema": {
              "type": "string",
              "const": "Huddlekit-Webhooks/1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CommentTextChangedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges the delivery. The response body is ignored."
          }
        }
      }
    },
    "comment.screenshot_ready": {
      "post": {
        "operationId": "onCommentScreenshotReady",
        "tags": [
          "Webhook events"
        ],
        "summary": "A comment's screenshot is ready",
        "description": "Sent the first time a website or web app comment gets a screenshot. Documents have no screenshots. Sent as a JSON POST to every active subscription (and every webhook set up in the Huddlekit app) that includes this event type, while the workspace is on the Team plan. Respond with any 2xx within 10 seconds. Any other status (redirects are not followed), an error or a timeout is a failure: the delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours (6 attempts in all) and then set aside, and an endpoint that has failed 20 times in a row over more than 24 hours is switched off. Delivery is at-least-once; de-duplicate on `X-Huddlekit-Event-Id`.",
        "security": [],
        "parameters": [
          {
            "name": "X-Huddlekit-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<unix seconds>,v1=<hex>`. `v1` is the HMAC-SHA256 of `<t>.<raw request body>`, keyed with the subscription's full signing secret (including the `whsec_` prefix). During a secret rotation the header carries a second `v1`; accept the request if any `v1` matches. Reject requests whose `t` is more than 5 minutes from your clock.",
            "schema": {
              "type": "string"
            },
            "example": "t=1790496000,v1=5f2b1c0e9a7d3f6b8c4e2a1d0f9b7c5e3a1d8f6b4c2e0a9d7f5b3c1e8a6d4f2b"
          },
          {
            "name": "X-Huddlekit-Event",
            "in": "header",
            "required": true,
            "description": "The event type, the same as `event` in the body.",
            "schema": {
              "$ref": "#/components/schemas/EventType"
            }
          },
          {
            "name": "X-Huddlekit-Event-Id",
            "in": "header",
            "required": true,
            "description": "The event id, the same as `event_id` in the body. Unchanged across retries; use it to de-duplicate.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "User-Agent",
            "in": "header",
            "required": true,
            "description": "Always `Huddlekit-Webhooks/1`.",
            "schema": {
              "type": "string",
              "const": "Huddlekit-Webhooks/1"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CommentScreenshotReadyEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Any 2xx acknowledges the delivery. The response body is ignored."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "hk_live_ + 64 hex characters",
        "description": "Workspace API key, created in the Huddlekit app and shown once. Send it as `Authorization: Bearer hk_live_<64 lowercase hex characters>`. The key identifies the workspace; there is no user session. GET calls need the `read` scope; POST, PATCH and DELETE calls need `write`."
      }
    },
    "parameters": {
      "Surface": {
        "name": "surface",
        "in": "query",
        "required": false,
        "description": "What `parent_id` refers to. Defaults to `website`.",
        "schema": {
          "$ref": "#/components/schemas/Surface",
          "default": "website"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was invalid. `error` says which field and why.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "parent_id is required"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No API key, a malformed `Authorization` header, or an invalid, revoked or expired key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Unauthorized",
              "detail": "Send your key as: Authorization: Bearer hk_live_…"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The key lacks the scope this call needs (`{\"error\":\"Forbidden\",\"detail\":\"This key lacks the \\\"read\\\" scope\"}`), or the workspace has no active Team subscription (body includes `requiredPlans`).",
        "content": {
          "application/json": {
            "schema": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/PlanRequiredError"
                },
                {
                  "$ref": "#/components/schemas/Error"
                }
              ]
            },
            "example": {
              "error": "This feature requires the Team plan.",
              "requiredPlans": [
                "team"
              ]
            }
          }
        }
      },
      "ForbiddenScope": {
        "description": "The key lacks the `write` scope.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Forbidden",
              "detail": "This key lacks the \"write\" scope"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found, or it belongs to another workspace (the two cases are indistinguishable on purpose). An id that isn't a valid UUID also answers 404.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Not found"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limit exceeded. Limits: 200 reads and 30 writes per minute per API key, counted per endpoint group (`/me`, `/projects`, `/comments`, `/comments/{id}`, `/hooks`, `/events/recent`) and separately for reads and writes; `DELETE /hooks/{id}` counts toward the `/hooks` writes. Refused calls count too. Wait the number of seconds in `Retry-After`, then retry.",
        "headers": {
          "Retry-After": {
            "description": "Whole seconds until the limit resets (at least 1).",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Too many requests"
            }
          }
        }
      },
      "InternalError": {
        "description": "The request could not be completed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "A temporary failure, such as the API key, the workspace plan or the parent record could not be checked. Safe to retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Could not verify the workspace plan"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Error body returned by every failed call.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Short, human-readable error message."
          },
          "detail": {
            "type": "string",
            "description": "Extra explanation, when there is one."
          }
        },
        "example": {
          "error": "Unauthorized",
          "detail": "Invalid or revoked API key"
        }
      },
      "PlanRequiredError": {
        "type": "object",
        "description": "Returned with 403 when the key's workspace is not on a plan that includes API access.",
        "required": [
          "error",
          "requiredPlans"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message naming the required plan."
          },
          "requiredPlans": {
            "type": "array",
            "description": "Plan ids that include API access.",
            "items": {
              "type": "string"
            }
          }
        },
        "example": {
          "error": "This feature requires the Team plan.",
          "requiredPlans": [
            "team"
          ]
        }
      },
      "Surface": {
        "type": "string",
        "enum": [
          "website",
          "webapp",
          "document"
        ],
        "description": "What a comment is attached to. `website`: a website project (the parent is a project). `webapp`: a web app that runs the Huddlekit SDK widget (the parent is a web app). `document`: an uploaded PDF, image or video (the parent is a document)."
      },
      "CommentStatus": {
        "type": "string",
        "enum": [
          "open",
          "in-review",
          "in-progress",
          "resolved"
        ],
        "description": "Workflow status of a comment. Setting `resolved` also marks the comment resolved; any other value marks it unresolved."
      },
      "EventType": {
        "type": "string",
        "enum": [
          "comment.created",
          "comment.status_changed",
          "comment.text_changed",
          "comment.screenshot_ready"
        ],
        "description": "`comment.created`: a comment was added. `comment.status_changed`: its status changed. `comment.text_changed`: its text was edited. `comment.screenshot_ready`: its screenshot finished capturing (website and webapp comments only). The **Send test event** button in the Huddlekit app also sends `ping`, with a made-up comment and no `source` or `permalink`; answer it with any 2xx and don't treat it as a real event."
      },
      "ApiKeyInfo": {
        "type": "object",
        "description": "The workspace and permissions of the API key that made the request.",
        "required": [
          "workspace_id",
          "workspace_name",
          "key_id",
          "scopes",
          "source"
        ],
        "properties": {
          "workspace_id": {
            "type": "string",
            "format": "uuid",
            "description": "Id of the workspace the key belongs to."
          },
          "workspace_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the workspace, or null if it could not be read."
          },
          "key_id": {
            "type": "string",
            "format": "uuid",
            "description": "Id of this API key (not the secret key itself)."
          },
          "scopes": {
            "type": "array",
            "description": "Scopes granted to this key. `read` allows GET calls; `write` allows POST, PATCH and DELETE calls.",
            "items": {
              "type": "string",
              "enum": [
                "read",
                "write"
              ]
            }
          },
          "source": {
            "type": "string",
            "description": "The `source` value that events caused by this key carry, `connector:<key_id>`. A consumer that both reads and writes comments should ignore events whose `source` equals this value, to avoid loops."
          }
        },
        "example": {
          "workspace_id": "9a4e2b7c-1d5f-4a8e-b3c6-7e0f2d9a1b58",
          "workspace_name": "Acme Studio",
          "key_id": "c2a8e4f1-7b3d-4e9a-b6c5-1f0d8e2a7b93",
          "scopes": [
            "read",
            "write"
          ],
          "source": "connector:c2a8e4f1-7b3d-4e9a-b6c5-1f0d8e2a7b93"
        }
      },
      "Project": {
        "type": "object",
        "description": "Something comments can be attached to: a website project, a web app or a document.",
        "required": [
          "id",
          "name",
          "url",
          "created_at",
          "surface"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Parent id. Pass it as `parent_id` (with the same `surface`) to list or create comments."
          },
          "name": {
            "type": "string",
            "description": "Display name."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Base URL of the website or web app. Always null for documents; may be null for a web app."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it was created."
          },
          "surface": {
            "$ref": "#/components/schemas/Surface"
          }
        }
      },
      "ListProjectsResponse": {
        "type": "object",
        "required": [
          "projects"
        ],
        "properties": {
          "projects": {
            "type": "array",
            "description": "Every website project, web app and document in the workspace, in that order, each group newest first. Not paginated.",
            "items": {
              "$ref": "#/components/schemas/Project"
            }
          }
        },
        "example": {
          "projects": [
            {
              "id": "0d3c7a52-9e61-4f0b-8a2d-5b7e1c4f9a36",
              "name": "Acme marketing site",
              "url": "https://acme.example",
              "created_at": "2026-09-01T09:30:00.000Z",
              "surface": "website"
            },
            {
              "id": "4e8b1d6a-2c7f-4b3e-9d1a-6f5c0e8b2a74",
              "name": "Brand guidelines.pdf",
              "url": null,
              "created_at": "2026-08-20T14:02:11.000Z",
              "surface": "document"
            }
          ]
        }
      },
      "WebsiteComment": {
        "type": "object",
        "description": "A comment on a website project (`surface: website`).",
        "additionalProperties": false,
        "required": [
          "id",
          "comment_number",
          "text",
          "status",
          "priority",
          "resolved",
          "is_private",
          "path",
          "screenshot",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Comment id."
          },
          "comment_number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Sequential number of the comment within its parent (shown as #42 in Huddlekit)."
          },
          "text": {
            "type": "string",
            "description": "The comment text as written."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "open",
              "in-review",
              "in-progress",
              "resolved",
              null
            ],
            "description": "Workflow status: `open`, `in-review`, `in-progress` or `resolved`."
          },
          "priority": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "Low",
              "Medium",
              "Critical",
              null
            ],
            "description": "Priority set in Huddlekit, or null when none is set."
          },
          "resolved": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the comment is resolved."
          },
          "is_private": {
            "type": "boolean",
            "description": "Whether the comment is hidden from guests."
          },
          "path": {
            "type": [
              "string",
              "null"
            ],
            "description": "Page path on the website, relative to the project's URL."
          },
          "screenshot": {
            "type": [
              "string",
              "null"
            ],
            "description": "Screenshot of the page, or null if none has been captured."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the comment was created."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the comment was last updated."
          }
        }
      },
      "WebappComment": {
        "type": "object",
        "description": "A comment on a web app (`surface: webapp`).",
        "additionalProperties": false,
        "required": [
          "id",
          "comment_number",
          "text",
          "status",
          "priority",
          "resolved",
          "is_private",
          "url",
          "path",
          "page_title",
          "screenshot",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Comment id."
          },
          "comment_number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Sequential number of the comment within its parent (shown as #42 in Huddlekit)."
          },
          "text": {
            "type": "string",
            "description": "The comment text as written."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "open",
              "in-review",
              "in-progress",
              "resolved",
              null
            ],
            "description": "Workflow status: `open`, `in-review`, `in-progress` or `resolved`."
          },
          "priority": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "Low",
              "Medium",
              "Critical",
              null
            ],
            "description": "Priority set in Huddlekit, or null when none is set."
          },
          "resolved": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the comment is resolved."
          },
          "is_private": {
            "type": "boolean",
            "description": "Whether the comment is hidden from guests."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full URL of the page the comment is on."
          },
          "path": {
            "type": "string",
            "description": "Page path."
          },
          "page_title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Title of the page the comment is on."
          },
          "screenshot": {
            "type": [
              "string",
              "null"
            ],
            "description": "Screenshot of the page, or null if none has been captured."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the comment was created."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the comment was last updated."
          }
        }
      },
      "DocumentComment": {
        "type": "object",
        "description": "A comment on a document (`surface: document`). Document comments have no path, URL or screenshot.",
        "additionalProperties": false,
        "required": [
          "id",
          "comment_number",
          "text",
          "status",
          "priority",
          "resolved",
          "is_private",
          "page_number",
          "video_timestamp",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Comment id."
          },
          "comment_number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Sequential number of the comment within its parent (shown as #42 in Huddlekit)."
          },
          "text": {
            "type": "string",
            "description": "The comment text as written."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "open",
              "in-review",
              "in-progress",
              "resolved",
              null
            ],
            "description": "Workflow status: `open`, `in-review`, `in-progress` or `resolved`."
          },
          "priority": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "Low",
              "Medium",
              "Critical",
              null
            ],
            "description": "Priority set in Huddlekit, or null when none is set."
          },
          "resolved": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the comment is resolved."
          },
          "is_private": {
            "type": "boolean",
            "description": "Whether the comment is hidden from guests."
          },
          "page_number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Page of the document the comment is on (1-based)."
          },
          "video_timestamp": {
            "type": [
              "number",
              "null"
            ],
            "description": "Position in seconds, for comments on a video; otherwise null."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the comment was created."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the comment was last updated."
          }
        }
      },
      "ListCommentsResponse": {
        "type": "object",
        "required": [
          "comments",
          "surface"
        ],
        "properties": {
          "comments": {
            "type": "array",
            "description": "Comments on the parent, newest first. The shape of each item depends on `surface`.",
            "items": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/WebsiteComment"
                },
                {
                  "$ref": "#/components/schemas/WebappComment"
                },
                {
                  "$ref": "#/components/schemas/DocumentComment"
                }
              ]
            }
          },
          "surface": {
            "$ref": "#/components/schemas/Surface"
          }
        },
        "example": {
          "comments": [
            {
              "id": "6b1f9c2e-4d7a-4c1b-9e3f-2a8d5c7b1e04",
              "comment_number": 42,
              "text": "The signup button overlaps the footer on mobile.",
              "status": "open",
              "priority": "Medium",
              "resolved": false,
              "is_private": false,
              "path": "/pricing",
              "screenshot": null,
              "created_at": "2026-09-20T10:15:00.000Z",
              "updated_at": "2026-09-20T10:15:00.000Z"
            }
          ],
          "surface": "website"
        }
      },
      "CreateCommentRequest": {
        "type": "object",
        "description": "A new comment. Which location fields are allowed depends on `surface`: `path` for website; `url`, `page_title` and `path` for webapp; `page_number` and `video_timestamp` for document. Sending a location field that belongs to another surface is a 400 (except `path` on a document, which is ignored). A null or empty-string location field counts as not sent.",
        "required": [
          "parent_id",
          "text"
        ],
        "properties": {
          "parent_id": {
            "type": "string",
            "format": "uuid",
            "description": "Id of the project, web app or document to comment on (from `GET /projects`). Must belong to the key's workspace."
          },
          "project_id": {
            "type": "string",
            "format": "uuid",
            "deprecated": true,
            "description": "Deprecated alias of `parent_id`, used only when `parent_id` is absent."
          },
          "surface": {
            "$ref": "#/components/schemas/Surface",
            "default": "website",
            "description": "What `parent_id` refers to. Defaults to `website`."
          },
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 10000,
            "description": "Comment text. Must not be blank. At most 10,000 characters."
          },
          "status": {
            "$ref": "#/components/schemas/CommentStatus",
            "default": "open",
            "description": "Initial status. Defaults to `open`."
          },
          "path": {
            "type": "string",
            "maxLength": 2048,
            "description": "Website and webapp only. Page path the comment is about, such as `/pricing`. Sanitized before it is stored. Defaults to `/` for a website, and to the path of `url` (or `/`) for a web app. Ignored for documents."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Webapp only. Full http:// or https:// URL of the page on your site that the comment is about."
          },
          "page_title": {
            "type": "string",
            "maxLength": 500,
            "description": "Webapp only. Title of the page. Trimmed; at most 500 characters."
          },
          "page_number": {
            "type": "integer",
            "minimum": 1,
            "description": "Document only. Page number, 1 or more. Defaults to 1."
          },
          "video_timestamp": {
            "type": "number",
            "minimum": 0,
            "description": "Document only, and only when the document is a video. Position in seconds, 0 or more."
          }
        },
        "example": {
          "parent_id": "0d3c7a52-9e61-4f0b-8a2d-5b7e1c4f9a36",
          "surface": "website",
          "text": "Hero headline wraps badly at 1024px.",
          "path": "/"
        }
      },
      "CreatedComment": {
        "type": "object",
        "description": "The comment that was created.",
        "required": [
          "id",
          "comment_number",
          "text",
          "status",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Id of the new comment."
          },
          "comment_number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Sequential number of the comment within its parent."
          },
          "text": {
            "type": "string",
            "description": "The comment text."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "open",
              "in-review",
              "in-progress",
              "resolved",
              null
            ],
            "description": "Workflow status: `open`, `in-review`, `in-progress` or `resolved`."
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the comment was created."
          }
        }
      },
      "CreateCommentResponse": {
        "type": "object",
        "required": [
          "comment",
          "surface"
        ],
        "properties": {
          "comment": {
            "$ref": "#/components/schemas/CreatedComment"
          },
          "surface": {
            "$ref": "#/components/schemas/Surface"
          }
        },
        "example": {
          "comment": {
            "id": "6b1f9c2e-4d7a-4c1b-9e3f-2a8d5c7b1e04",
            "comment_number": 43,
            "text": "Hero headline wraps badly at 1024px.",
            "status": "open",
            "created_at": "2026-09-27T08:00:00.000Z"
          },
          "surface": "website"
        }
      },
      "UpdateCommentStatusRequest": {
        "type": "object",
        "description": "The new status. `status` is the only writable field; sending `text` is refused with a 400. Other fields are ignored.",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "$ref": "#/components/schemas/CommentStatus",
            "description": "New status for the comment."
          },
          "surface": {
            "$ref": "#/components/schemas/Surface",
            "default": "website",
            "description": "Surface the comment belongs to. Defaults to `website`. A comment id looked up on the wrong surface returns 404."
          }
        },
        "example": {
          "status": "resolved",
          "surface": "website"
        }
      },
      "UpdatedComment": {
        "type": "object",
        "description": "The comment after the update.",
        "required": [
          "id",
          "comment_number",
          "text",
          "status",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Comment id."
          },
          "comment_number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Sequential number of the comment within its parent."
          },
          "text": {
            "type": "string",
            "description": "The comment text (unchanged)."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "open",
              "in-review",
              "in-progress",
              "resolved",
              null
            ],
            "description": "Workflow status: `open`, `in-review`, `in-progress` or `resolved`."
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the comment was last updated."
          }
        }
      },
      "UpdateCommentStatusResponse": {
        "type": "object",
        "required": [
          "comment",
          "surface"
        ],
        "properties": {
          "comment": {
            "$ref": "#/components/schemas/UpdatedComment"
          },
          "surface": {
            "$ref": "#/components/schemas/Surface"
          }
        },
        "example": {
          "comment": {
            "id": "6b1f9c2e-4d7a-4c1b-9e3f-2a8d5c7b1e04",
            "comment_number": 42,
            "text": "The signup button overlaps the footer on mobile.",
            "status": "resolved",
            "updated_at": "2026-09-27T08:05:00.000Z"
          },
          "surface": "website"
        }
      },
      "WebhookSubscription": {
        "type": "object",
        "description": "A webhook subscription created with this API key.",
        "required": [
          "id",
          "url",
          "event_types",
          "is_active",
          "created_at",
          "disabled_at",
          "consecutive_failures"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Subscription id. Pass it to `DELETE /hooks/{id}` to unsubscribe."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The https:// URL events are POSTed to."
          },
          "event_types": {
            "type": "array",
            "description": "Event types delivered to this URL. An empty array means every event type.",
            "items": {
              "$ref": "#/components/schemas/EventType"
            }
          },
          "is_active": {
            "type": "boolean",
            "description": "False when paused in the Huddlekit app. A switched-off subscription keeps `true` and has `disabled_at` set."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the subscription was created."
          },
          "disabled_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the subscription was disabled, or null."
          },
          "consecutive_failures": {
            "type": "integer",
            "minimum": 0,
            "description": "Failed deliveries in a row. The subscription is switched off after 20 failures in a row spanning more than 24 hours."
          }
        }
      },
      "ListWebhookSubscriptionsResponse": {
        "type": "object",
        "required": [
          "hooks"
        ],
        "properties": {
          "hooks": {
            "type": "array",
            "description": "Subscriptions created with this API key, newest first. Webhooks created in the Huddlekit app or with other keys are not included.",
            "items": {
              "$ref": "#/components/schemas/WebhookSubscription"
            }
          }
        }
      },
      "CreateWebhookSubscriptionRequest": {
        "type": "object",
        "description": "The URL to deliver events to and, optionally, which events. `target_url` and `url` are accepted as alternative spellings of `targetUrl`, and `events` as an alternative spelling of `event_types`.",
        "required": [
          "targetUrl"
        ],
        "properties": {
          "targetUrl": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://.+",
            "description": "The https:// URL to POST events to. Must be a public URL outside Huddlekit."
          },
          "target_url": {
            "type": "string",
            "format": "uri",
            "description": "Alternative spelling of `targetUrl`, used when `targetUrl` is absent."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Alternative spelling of `targetUrl`, used when `targetUrl` and `target_url` are absent."
          },
          "event_types": {
            "type": "array",
            "description": "Event types to deliver. Omit it, or send an empty array, to receive every event type. Unknown values are a 400.",
            "items": {
              "$ref": "#/components/schemas/EventType"
            }
          },
          "events": {
            "type": "array",
            "description": "Alternative spelling of `event_types`, used when `event_types` is absent.",
            "items": {
              "$ref": "#/components/schemas/EventType"
            }
          }
        },
        "example": {
          "targetUrl": "https://hooks.example.com/huddlekit",
          "event_types": [
            "comment.created",
            "comment.status_changed"
          ]
        }
      },
      "WebhookSubscriptionCreated": {
        "type": "object",
        "description": "A new subscription. This is the only time the signing secret is returned.",
        "required": [
          "id",
          "url",
          "event_types",
          "secret",
          "secret_shown_once",
          "known_platform"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Subscription id."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The URL events will be POSTed to."
          },
          "event_types": {
            "type": "array",
            "description": "Event types delivered. Empty means every event type.",
            "items": {
              "$ref": "#/components/schemas/EventType"
            }
          },
          "secret": {
            "type": "string",
            "description": "Signing secret (`whsec_` followed by 64 hex characters) used for the `X-Huddlekit-Signature` header on deliveries to this URL. Shown once and cannot be retrieved again; store it now."
          },
          "secret_shown_once": {
            "type": "boolean",
            "const": true,
            "description": "Always true: the secret will not be shown again."
          },
          "known_platform": {
            "type": "boolean",
            "description": "True when the subscription was made by a platform Huddlekit recognises, such as Zapier."
          }
        },
        "example": {
          "id": "6b1f9c2e-4d7a-4c1b-9e3f-2a8d5c7b1e04",
          "url": "https://hooks.example.com/huddlekit",
          "event_types": [
            "comment.created",
            "comment.status_changed"
          ],
          "secret": "whsec_0000000000000000000000000000000000000000000000000000000000000000",
          "secret_shown_once": true,
          "known_platform": false
        }
      },
      "WebhookSubscriptionResubscribed": {
        "type": "object",
        "description": "This key already had a subscription for the same URL. It was reactivated and its event types replaced. The existing signing secret is kept and is not returned.",
        "required": [
          "id",
          "url",
          "resubscribed"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Id of the existing subscription."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The subscribed URL."
          },
          "resubscribed": {
            "type": "boolean",
            "const": true,
            "description": "Always true."
          }
        }
      },
      "DeleteWebhookSubscriptionResponse": {
        "type": "object",
        "required": [
          "deleted",
          "id"
        ],
        "properties": {
          "deleted": {
            "type": "boolean",
            "description": "True if a subscription was removed; false if none matched (already deleted, or not created by this key)."
          },
          "id": {
            "type": "string",
            "description": "The id from the request path."
          }
        },
        "example": {
          "deleted": true,
          "id": "6b1f9c2e-4d7a-4c1b-9e3f-2a8d5c7b1e04"
        }
      },
      "WebsitePage": {
        "type": "object",
        "title": "Website page",
        "description": "Location of a website comment.",
        "additionalProperties": false,
        "required": [
          "path"
        ],
        "properties": {
          "path": {
            "type": [
              "string",
              "null"
            ],
            "description": "Page path, relative to the project's URL."
          }
        }
      },
      "WebappPage": {
        "type": "object",
        "title": "Web app page",
        "description": "Location of a web app comment.",
        "additionalProperties": false,
        "required": [
          "url",
          "path",
          "title"
        ],
        "properties": {
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full URL of the page."
          },
          "path": {
            "type": [
              "string",
              "null"
            ],
            "description": "Page path."
          },
          "title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Page title."
          }
        }
      },
      "DocumentPage": {
        "type": "object",
        "title": "Document page",
        "description": "Location of a document comment.",
        "additionalProperties": false,
        "required": [
          "page_number"
        ],
        "properties": {
          "page_number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Page number (1-based)."
          },
          "video_timestamp": {
            "type": "number",
            "description": "Position in seconds. Present only for comments on a video."
          }
        }
      },
      "WebhookAuthor": {
        "type": "object",
        "description": "Who wrote the comment.",
        "required": [
          "name",
          "kind"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Display name, or `Someone` when the name is unknown. Comments created through this API show as `API`."
          },
          "kind": {
            "type": "string",
            "enum": [
              "user",
              "guest"
            ],
            "description": "`user` for a Huddlekit member, `guest` for anyone else."
          }
        }
      },
      "WebhookComment": {
        "type": "object",
        "description": "The comment as it is when the event is sent.",
        "required": [
          "id",
          "number",
          "title",
          "text",
          "status",
          "page",
          "permalink",
          "screenshot",
          "browser_info",
          "author",
          "created_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Comment id."
          },
          "number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Sequential number of the comment within its parent."
          },
          "title": {
            "type": "string",
            "description": "One-line title made from the text: its first line, shortened to about 80 characters, ending in … when anything was cut."
          },
          "text": {
            "type": "string",
            "description": "Full comment text."
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "open",
              "in-review",
              "in-progress",
              "resolved",
              null
            ],
            "description": "Workflow status: `open`, `in-review`, `in-progress` or `resolved`."
          },
          "page": {
            "description": "Where the comment is. Shape depends on `surface`.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/WebsitePage"
              },
              {
                "$ref": "#/components/schemas/WebappPage"
              },
              {
                "$ref": "#/components/schemas/DocumentPage"
              }
            ]
          },
          "permalink": {
            "type": "string",
            "format": "uri",
            "description": "Link back to the comment. For website and document comments it opens the comment in Huddlekit; for web app comments it opens your page with the Huddlekit widget."
          },
          "screenshot": {
            "type": [
              "string",
              "null"
            ],
            "description": "Screenshot of the page, or null. Always null for documents."
          },
          "browser_info": {
            "description": "Browser and device details recorded with the comment (JSON), or null. Always null for documents."
          },
          "author": {
            "description": "Who wrote the comment, or null when unknown.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/WebhookAuthor"
              },
              {
                "type": "null"
              }
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the comment was created."
          }
        }
      },
      "Change": {
        "type": "object",
        "description": "Before and after values of a changed field.",
        "required": [
          "old",
          "new"
        ],
        "properties": {
          "old": {
            "description": "Previous value. Always null for text edits (the pre-edit text is never sent) and for screenshots."
          },
          "new": {
            "description": "New value."
          }
        }
      },
      "WebhookPayload": {
        "type": "object",
        "description": "Body of every webhook delivery, and of each item returned by `GET /events/recent`.",
        "required": [
          "event",
          "event_id",
          "occurred_at",
          "workspace_id",
          "surface",
          "parent_id",
          "source",
          "comment",
          "changed"
        ],
        "properties": {
          "event": {
            "$ref": "#/components/schemas/EventType"
          },
          "event_id": {
            "type": "string",
            "description": "Unique event id; the same on every retry, so use it to de-duplicate. Samples from `GET /events/recent` use `sample:<comment id>`."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the change happened."
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid",
            "description": "Workspace the comment belongs to."
          },
          "surface": {
            "$ref": "#/components/schemas/Surface"
          },
          "parent_id": {
            "type": "string",
            "description": "Id of the project, web app or document the comment is on."
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who made the change: `app` (a change made in Huddlekit, or synced back from Slack, Linear, ClickUp or Notion), `mcp` (an AI agent via Huddlekit's MCP server) or `connector:<api_key_id>` (a call to this API with that key). Null only on events from before 2026-09-06."
          },
          "comment": {
            "description": "The comment. Typed as nullable, but events whose comment no longer exists are not sent.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/WebhookComment"
              },
              {
                "type": "null"
              }
            ]
          },
          "changed": {
            "type": [
              "object",
              "null"
            ],
            "description": "What changed, keyed by field: `status` for comment.status_changed, `text` for comment.text_changed (with `old` always null), `screenshot` for comment.screenshot_ready (with `old` null). Null for comment.created.",
            "additionalProperties": {
              "$ref": "#/components/schemas/Change"
            }
          }
        }
      },
      "CommentCreatedEvent": {
        "description": "Payload of a comment.created delivery. `changed` is null.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookPayload"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "comment.created"
              },
              "changed": {
                "type": "null"
              }
            }
          }
        ]
      },
      "CommentStatusChangedEvent": {
        "description": "Payload of a comment.status_changed delivery.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookPayload"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "comment.status_changed"
              },
              "changed": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "object",
                    "description": "Previous and new status.",
                    "required": [
                      "old",
                      "new"
                    ],
                    "properties": {
                      "old": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Previous status."
                      },
                      "new": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "New status."
                      }
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "CommentTextChangedEvent": {
        "description": "Payload of a comment.text_changed delivery.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookPayload"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "comment.text_changed"
              },
              "changed": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "object",
                    "description": "The new text. The previous text is never sent.",
                    "required": [
                      "old",
                      "new"
                    ],
                    "properties": {
                      "old": {
                        "type": "null",
                        "description": "Always null."
                      },
                      "new": {
                        "type": "string",
                        "description": "New comment text."
                      }
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "CommentScreenshotReadyEvent": {
        "description": "Payload of a comment.screenshot_ready delivery. Sent the first time a website or web app comment gets a screenshot.",
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookPayload"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "const": "comment.screenshot_ready"
              },
              "changed": {
                "type": "object",
                "required": [
                  "screenshot"
                ],
                "properties": {
                  "screenshot": {
                    "type": "object",
                    "description": "The screenshot that was just captured.",
                    "required": [
                      "old",
                      "new"
                    ],
                    "properties": {
                      "old": {
                        "type": "null",
                        "description": "Always null."
                      },
                      "new": {
                        "type": "string",
                        "description": "The new screenshot."
                      }
                    }
                  }
                }
              },
              "surface": {
                "enum": [
                  "website",
                  "webapp"
                ]
              }
            }
          }
        ]
      },
      "ListRecentEventsResponse": {
        "type": "object",
        "required": [
          "events",
          "sample"
        ],
        "properties": {
          "events": {
            "type": "array",
            "description": "Sample payloads built from the newest comments, newest first.",
            "items": {
              "$ref": "#/components/schemas/WebhookPayload"
            }
          },
          "sample": {
            "type": "boolean",
            "const": true,
            "description": "Always true: these are samples, not replayed deliveries."
          }
        }
      }
    }
  }
}
