{"openapi":"3.1.0","info":{"title":"Hollahoop REST API","version":"1","description":"Read and write your feedback, roadmap, and changelog programmatically. These REST endpoints mirror the Hollahoop MCP tools one-to-one and share the same Bearer token auth, so you can use whichever surface fits your stack. Responses are JSON; errors use a consistent { error: { code, message } } envelope.","contact":{"name":"Hollahoop","url":"https://hollahoop.app/api-docs"}},"servers":[{"url":"https://hollahoop.app/api/v1","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"System","description":"Unauthenticated liveness and readiness probes."},{"name":"Projects","description":"List and create projects in the token organisation."},{"name":"Posts","description":"Read feedback and roadmap posts."},{"name":"Statuses","description":"Read the roadmap status workflow."},{"name":"Changelog","description":"Read changelog / Updates entries."},{"name":"CI","description":"Record CI run status for a project."},{"name":"Domains","description":"List, add, and verify custom hostnames for a project."}],"paths":{"/health":{"get":{"tags":["System"],"summary":"Liveness probe","description":"Always returns 200 while the app is serving traffic. No auth required.","operationId":"getHealth","security":[],"responses":{"200":{"description":"Service is live.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"version":{"type":"string","example":"1"},"ts":{"type":"string","format":"date-time"}},"required":["ok","version","ts"]}}}}}}},"/ready":{"get":{"tags":["System"],"summary":"Readiness probe","description":"Checks downstream dependencies (PostgREST, optional pooler, Redis). Returns 200 when ready and 503 when degraded. No auth required unless READINESS_PROBE_TOKEN is configured for the deployment.","operationId":"getReady","security":[],"responses":{"200":{"description":"All dependency checks passed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Readiness"}}}},"503":{"description":"One or more dependency checks failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Readiness"}}}}}}},"/projects":{"get":{"tags":["Projects"],"summary":"List projects","description":"List every project the API key can see (scoped to a single Hollahoop organisation).","operationId":"listProjects","security":[{"bearerAuth":["read"]}],"responses":{"200":{"description":"Projects visible to this token.","headers":{"x-ratelimit-limit":{"$ref":"#/components/headers/RateLimitLimit"},"x-ratelimit-remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"x-ratelimit-reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Projects"],"summary":"Create a project","description":"Create a new project under this API key’s organisation. Returns the project plus the URL of its public board. Default boards, statuses, changelog tags, and docs section are auto-seeded.","operationId":"createProject","security":[{"bearerAuth":["write"]}],"requestBody":{"required":true,"description":"Project attributes. Boards, statuses, tags, and a docs section are auto-seeded.","content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"type":"string","minLength":3,"maxLength":40,"pattern":"^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$","description":"URL slug - lives at /<slug> on the public site."},"name":{"type":"string","minLength":2,"maxLength":80,"description":"Display name shown in the UI."},"tagline":{"description":"Short marketing line for the public board.","type":"string","maxLength":120},"description":{"description":"Optional longer description.","type":"string","maxLength":2000},"accent_color":{"description":"Hex colour for chips / accents, e.g. #6366f1.","type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"is_public":{"description":"Make the public board visible - defaults to true.","type":"boolean"}},"required":["slug","name"]}}}},"responses":{"201":{"description":"The created project plus its public board URL.","headers":{"x-ratelimit-limit":{"$ref":"#/components/headers/RateLimitLimit"},"x-ratelimit-remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"x-ratelimit-reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/projects/{projectSlug}/posts":{"get":{"tags":["Posts"],"summary":"List posts","description":"List feedback posts on a project, optionally filtered by board and/or status. Default sort is by vote count.","operationId":"listPosts","security":[{"bearerAuth":["read"]}],"parameters":[{"name":"projectSlug","in":"path","required":true,"description":"Slug of a project inside the token organisation.","schema":{"type":"string","minLength":1}},{"name":"board","in":"query","required":false,"description":"Optional board slug (e.g. feature-requests)","schema":{"type":"string"}},{"name":"status","in":"query","required":false,"description":"Optional status key (e.g. open, planned)","schema":{"type":"string"}},{"name":"post_type","in":"query","required":false,"description":"Optional post type filter","schema":{"type":"string","enum":["feature","bug","tweak"]}},{"name":"sort","in":"query","required":false,"schema":{"default":"top","type":"string","enum":["top","new","oldest"]}},{"name":"limit","in":"query","required":false,"schema":{"default":25,"type":"integer","minimum":1,"maximum":100}},{"name":"offset","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":9007199254740991}}],"responses":{"200":{"description":"Matching posts with the applied filter echoed back.","headers":{"x-ratelimit-limit":{"$ref":"#/components/headers/RateLimitLimit"},"x-ratelimit-remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"x-ratelimit-reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/projects/{projectSlug}/posts/{postId}":{"get":{"tags":["Posts"],"summary":"Get a post","description":"Fetch a single post by id, including its comments.","operationId":"getPost","security":[{"bearerAuth":["read"]}],"parameters":[{"name":"projectSlug","in":"path","required":true,"description":"Slug of a project inside the token organisation.","schema":{"type":"string","minLength":1}},{"name":"postId","in":"path","required":true,"description":"UUID of the post.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The post plus its comments.","headers":{"x-ratelimit-limit":{"$ref":"#/components/headers/RateLimitLimit"},"x-ratelimit-remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"x-ratelimit-reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/projects/{projectSlug}/statuses":{"get":{"tags":["Statuses"],"summary":"List statuses","description":"List the post-status workflow for a project (Open / Planned / In progress / Shipped / Declined and any custom ones).","operationId":"listStatuses","security":[{"bearerAuth":["read"]}],"parameters":[{"name":"projectSlug","in":"path","required":true,"description":"Slug of a project inside the token organisation.","schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"The roadmap status workflow for the project.","headers":{"x-ratelimit-limit":{"$ref":"#/components/headers/RateLimitLimit"},"x-ratelimit-remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"x-ratelimit-reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/projects/{projectSlug}/changelog":{"get":{"tags":["Changelog"],"summary":"List changelog entries","description":"List changelog / Updates entries for a project (drafts and published).","operationId":"listChangelog","security":[{"bearerAuth":["read"]}],"parameters":[{"name":"projectSlug","in":"path","required":true,"description":"Slug of a project inside the token organisation.","schema":{"type":"string","minLength":1}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["draft","published"]}},{"name":"limit","in":"query","required":false,"schema":{"default":25,"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Changelog / Updates entries for the project.","headers":{"x-ratelimit-limit":{"$ref":"#/components/headers/RateLimitLimit"},"x-ratelimit-remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"x-ratelimit-reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/projects/{projectSlug}/ci":{"post":{"tags":["CI"],"summary":"Report CI status","description":"Record a CI run (GitHub Actions, GitLab CI, or generic) against a project. Same payload as POST /api/integrations/ci/webhook.","operationId":"reportCiStatus","security":[{"bearerAuth":["write"]}],"parameters":[{"name":"projectSlug","in":"path","required":true,"description":"Slug of a project inside the token organisation.","schema":{"type":"string","minLength":1}}],"requestBody":{"required":true,"description":"CI run: provider, repo, sha, status, optional url and post_id.","content":{"application/json":{"schema":{"type":"object","properties":{"project":{"type":"string","minLength":1},"provider":{"type":"string","minLength":1,"maxLength":40},"repo":{"type":"string","minLength":1,"maxLength":200},"sha":{"type":"string","minLength":7,"maxLength":64},"status":{"type":"string","enum":["success","failure","pending"]},"url":{"type":"string","maxLength":2000,"format":"uri"},"post_id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"required":["project","provider","repo","sha","status"]}}}},"responses":{"201":{"description":"Recorded CI event id.","headers":{"x-ratelimit-limit":{"$ref":"#/components/headers/RateLimitLimit"},"x-ratelimit-remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"x-ratelimit-reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/projects/{projectSlug}/domains":{"get":{"tags":["Domains"],"summary":"List custom domains","description":"List custom hostnames for a project, including TXT challenge values for pending rows.","operationId":"listDomains","security":[{"bearerAuth":["read"]}],"parameters":[{"name":"projectSlug","in":"path","required":true,"description":"Slug of a project inside the token organisation.","schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"Custom hostnames for the project.","headers":{"x-ratelimit-limit":{"$ref":"#/components/headers/RateLimitLimit"},"x-ratelimit-remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"x-ratelimit-reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"tags":["Domains"],"summary":"Add a custom domain","description":"Add a custom hostname to a project. Returns the TXT name and value to publish, then call verify_domain.","operationId":"addDomain","security":[{"bearerAuth":["write"]}],"parameters":[{"name":"projectSlug","in":"path","required":true,"description":"Slug of a project inside the token organisation.","schema":{"type":"string","minLength":1}}],"requestBody":{"required":true,"description":"Hostname the customer owns. TXT challenge fields are returned.","content":{"application/json":{"schema":{"type":"object","properties":{"project":{"type":"string","minLength":1,"description":"Project slug"},"hostname":{"type":"string","minLength":1,"maxLength":253,"description":"Hostname the customer owns, e.g. board.example.com"}},"required":["project","hostname"]}}}},"responses":{"201":{"description":"The pending hostname plus TXT instructions.","headers":{"x-ratelimit-limit":{"$ref":"#/components/headers/RateLimitLimit"},"x-ratelimit-remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"x-ratelimit-reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/projects/{projectSlug}/domains/{domainId}/verify":{"post":{"tags":["Domains"],"summary":"Verify a custom domain","description":"Check the TXT challenge for a pending custom hostname and mark it verified when the record matches.","operationId":"verifyDomain","security":[{"bearerAuth":["write"]}],"parameters":[{"name":"projectSlug","in":"path","required":true,"description":"Slug of a project inside the token organisation.","schema":{"type":"string","minLength":1}},{"name":"domainId","in":"path","required":true,"description":"UUID of the project_domains row.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Verified hostname, or already verified.","headers":{"x-ratelimit-limit":{"$ref":"#/components/headers/RateLimitLimit"},"x-ratelimit-remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"x-ratelimit-reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationError"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Organisation-scoped API key. Create one from your project settings and send it as `Authorization: Bearer hh_live_...`. The read scope covers GET endpoints; the write scope is required for POST."}},"headers":{"RateLimitLimit":{"description":"Request ceiling for this scope in the current window.","schema":{"type":"integer"}},"RateLimitRemaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer"}},"RateLimitReset":{"description":"Unix epoch (seconds) when the current window resets.","schema":{"type":"integer"}},"RetryAfter":{"description":"Seconds to wait before retrying after a 429.","schema":{"type":"integer"}}},"schemas":{"Error":{"type":"object","description":"Consistent error envelope returned for every non-2xx response.","properties":{"error":{"type":"object","properties":{"code":{"type":"string","description":"Stable, machine-readable error code.","example":"not_found"},"message":{"type":"string","description":"Human-readable explanation. Safe to log, not for end users."}},"required":["code","message"]}},"required":["error"]},"Readiness":{"type":"object","properties":{"ready":{"type":"boolean"},"status":{"type":"string","example":"ok"},"checks":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"ok":{"type":"boolean"},"detail":{"type":"string"}}}},"ts":{"type":"string","format":"date-time"}},"required":["ready","status","ts"]}},"responses":{"Unauthorized":{"description":"Missing, malformed, or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"The API key lacks the scope required for this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"The requested project or resource does not exist in this organisation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Conflict":{"description":"A resource with the same unique key already exists (e.g. project slug).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ValidationError":{"description":"The request body or query parameters failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Per-key rate limit exceeded (120 read/min, 30 write/min).","headers":{"x-ratelimit-limit":{"$ref":"#/components/headers/RateLimitLimit"},"x-ratelimit-remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"x-ratelimit-reset":{"$ref":"#/components/headers/RateLimitReset"},"retry-after":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}