{"openapi":"3.1.0","info":{"title":"MantelMarketing Public API","version":"1.0.0","description":"Public integration points for the MantelMarketing platform — what you can call from your own systems without a Clerk session. Internal admin + dashboard routes are not included here; ask in /contact if you need access."},"servers":[{"url":"https://app.mantelmarketing.com","description":"Production"},{"url":"http://localhost:3000","description":"Local development"}],"components":{"schemas":{"ErrorResponse":{"type":"object","properties":{"error":{"type":"string","description":"Machine-readable error code"},"message":{"type":"string"},"details":{}},"required":["error"]},"OkResponse":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"]},"HealthCheckResult":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded","down"]},"latency_ms":{"type":"number"},"error":{"type":"string"}},"required":["status","latency_ms"]},"HealthResponse":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded","down"]},"checks":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/HealthCheckResult"}},"release":{"type":"string"},"timestamp":{"type":"string"}},"required":["status","checks","release","timestamp"]}},"parameters":{}},"paths":{"/api/contact":{"post":{"summary":"Send a contact form submission","description":"Posts a contact-form message — no auth required. Rate-limited to 3 submissions per hour per IP. The honeypot `website` field must be empty; bots that auto-fill it are silently 200’d.","tags":["Marketing","Public"],"x-audience":"public","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","maxLength":254,"format":"email"},"message":{"type":"string","minLength":10,"maxLength":5000},"website":{"type":"string"}},"required":["name","email","message"]}}}},"responses":{"200":{"description":"Submission accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResponse"}}}},"400":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Email dispatch failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/status/subscribe":{"post":{"summary":"Subscribe an email address to status-page incident notifications","description":"Double-opt-in: this endpoint creates an unconfirmed row and emails the address a confirmation link. The address only receives further mail after `GET /api/status/confirm`. Re-subscribing an existing address re-issues the token.","tags":["Status","Public"],"x-audience":"public","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","maxLength":254,"format":"email"}},"required":["email"]}}}},"responses":{"200":{"description":"Confirmation email queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OkResponse"}}}},"400":{"description":"Invalid email","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/api/status/confirm":{"get":{"summary":"Confirm a status-page subscription","description":"Customers click this link from the confirmation email. Sets `confirmed_at = now()` on the matching subscriber row and 302-redirects to `/status?subscribed=1`.","tags":["Status","Public"],"x-audience":"public","parameters":[{"schema":{"type":"string","minLength":16,"description":"Per-row unsubscribe token issued at subscribe time"},"required":true,"description":"Per-row unsubscribe token issued at subscribe time","name":"token","in":"query"}],"responses":{"302":{"description":"Redirect to /status with a subscribed query param"}}}},"/api/status/unsubscribe":{"get":{"summary":"One-click unsubscribe from status-page notifications","description":"CAN-SPAM-compliant one-click unsubscribe. Deletes the matching subscriber row and 302-redirects to `/status?unsubscribed=1`. Always returns success-looking, even on an unknown token, to avoid leaking list membership.","tags":["Status","Public"],"x-audience":"public","parameters":[{"schema":{"type":"string","minLength":16},"required":true,"name":"token","in":"query"}],"responses":{"302":{"description":"Redirect to /status?unsubscribed=1"}}}},"/api/health":{"get":{"summary":"Readiness probe — checks Supabase, Clerk, Stripe, R2, Resend, Cloudflare, Slack","description":"Public liveness + readiness check. Returns 200 with `status: ok | degraded` and 503 with `status: down` when a critical dependency is unreachable. Cloudflare Worker uptime checks treat anything other than 200 + `status === \"ok\"` as a failure.","tags":["Ops","Public"],"x-audience":"public","responses":{"200":{"description":"Healthy or degraded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}},"503":{"description":"A critical dependency is down","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}}}}}},"/api/domains/search":{"get":{"summary":"Check whether a domain is available via Cloudflare Registrar","description":"Looks up live availability + price for a single domain through the Cloudflare Registrar API. Rate-limited to 60 requests per hour per user (or per IP for unauthenticated callers).","tags":["Domains","Public"],"x-audience":"public","parameters":[{"schema":{"type":"string","description":"FQDN to check, e.g. `acmebakery.com`"},"required":true,"description":"FQDN to check, e.g. `acmebakery.com`","name":"domain","in":"query"}],"responses":{"200":{"description":"Availability result","content":{"application/json":{"schema":{"type":"object","properties":{"available":{"type":"boolean"},"price":{"type":"number"},"currency":{"type":"string"}},"required":["available"]}}}},"400":{"description":"Missing domain","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/changelog/rss.xml":{"get":{"summary":"RSS 2.0 changelog feed","description":"Releases are published to a long-lived RSS 2.0 feed. Cache-control is `public, max-age=3600, s-maxage=3600`. Pair with the Atom variant for clients that prefer it.","tags":["Changelog","Public"],"x-audience":"public","responses":{"200":{"description":"RSS feed","content":{"application/rss+xml":{"schema":{"type":"string","description":"RSS 2.0 XML document"}}}}}}},"/changelog/atom.xml":{"get":{"summary":"Atom 1.0 changelog feed","description":"Same release stream as the RSS feed, encoded as Atom 1.0. Cache-control is `public, max-age=3600, s-maxage=3600`.","tags":["Changelog","Public"],"x-audience":"public","responses":{"200":{"description":"Atom feed","content":{"application/atom+xml":{"schema":{"type":"string","description":"Atom 1.0 XML document"}}}}}}}},"webhooks":{}}