/api/contactSend a contact form submission
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.
A small public surface for the things you might integrate with — the contact form, the status page, domain lookup, and the changelog feed. Site management lives inside the dashboard for now; if you need programmatic access to projects, files, or messages, get in touch.
Every endpoint documented in the reference grid below is reachable without a Clerk session. Two of them carry a per-row token in the URL — the status confirm and unsubscribe links — and that token is issued during the subscribe flow. Everything else is open by design.
We rate-limit by IP. Contact form submissions cap at three per hour; the domain availability check at sixty per hour; status subscribe at three per hour. If you hit a 429, slow down — there’s no premium tier you can buy out of it; the limit is there to keep the abuse floor low for everyone.
Customer-facing routes that normally require a Clerk dashboard session — publishing, posting messages, presigning file uploads — also accept a personal access token on the Authorization header. Generate one in your dashboard under API tokens, then call our API from a CI pipeline, Zapier zap, or any other machine context.
Tokens look like mm_pat_ followed by 32 base32 chars. Pass the full string with the Bearer scheme — the scheme name is case-insensitive but the token itself is case-sensitive.
curl -X POST https://mantelmarketing.com/api/publish \
-H "Authorization: Bearer mm_pat_YOUR_TOKEN_HERE" \
-H "Content-Type: application/json" \
-d '{"projectId":"YOUR_PROJECT_UUID"}'Scopes constrain capabilities. Each token carries a list of scopes chosen at creation. A request fails with 403 if the token lacks the scope its endpoint requires — even when the token’s account would otherwise have permission. The five scopes are:
publish — required for POST /api/publish.messages — required for POST /api/projects/{id}/messages.files — required for POST /api/files/presign.domain — required for POST /api/domains/purchase. Off by default.billing.read — read-only billing endpoints.We show the plaintext exactly once at creation time. Store it in a secret manager and never commit it to source control. If a token leaks, revoke it from the dashboard — it stops working immediately. Practical guide: Using API tokens.
Everything below is generated from the same OpenAPI spec the dashboard uses internally. The machine-readable version is at /api/openapi/public if you’d rather feed it into Postman or an SDK generator.
/api/contactPosts 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.
/api/status/subscribeDouble-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.
/api/status/confirmCustomers click this link from the confirmation email. Sets `confirmed_at = now()` on the matching subscriber row and 302-redirects to `/status?subscribed=1`.
| Name | In | Required | Description |
|---|---|---|---|
| tokenstring | query | required | Per-row unsubscribe token issued at subscribe time |
No body — just the status code.
/api/status/unsubscribeCAN-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.
| Name | In | Required | Description |
|---|---|---|---|
| tokenstring | query | required | — |
No body — just the status code.
/api/healthPublic 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.
/api/domains/searchLooks 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).
| Name | In | Required | Description |
|---|---|---|---|
| domainstring | query | required | FQDN to check, e.g. `acmebakery.com` |
/changelog/rss.xmlReleases 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.
No body — just the status code.
/changelog/atom.xmlSame release stream as the RSS feed, encoded as Atom 1.0. Cache-control is `public, max-age=3600, s-maxage=3600`.
No body — just the status code.
Register an HTTPS endpoint at /app/integrations to receive signed event POSTs. Every delivery carries X-MM-Signature: sha256=<hex> — the HMAC-SHA256 of the raw body using your endpoint’s signing secret. Verify it before trusting the payload. We retry up to three times on 5xx / network errors with exponential backoff; we don’t retry 4xx (we assume your endpoint is rejecting us deliberately). Endpoints that fail ten consecutive deliveries are auto-disabled.
Already signed in? The event reference inside your dashboard renders the full delivery body for each event with copy-pasteable sample headers and signature-verification snippets in Node.js + Python.
project.ready_for_reviewFires when our team marks your site ready for your review.
| Field | Type | Description |
|---|---|---|
project_id | uuid | Project identifier. |
project_name | string | Customer-facing project name. |
review_url | url | Deep link to the review page. |
project.approvedFires when you approve a site preview.
| Field | Type | Description |
|---|---|---|
project_id | uuid | Project identifier. |
project_name | string | Customer-facing project name. |
project.revisions_requestedFires when you request changes on a site preview.
| Field | Type | Description |
|---|---|---|
project_id | uuid | Project identifier. |
project_name | string | Customer-facing project name. |
message | string | Your revision-request message. |
project.publishedFires when your site goes live.
| Field | Type | Description |
|---|---|---|
project_id | uuid | Project identifier. |
project_name | string | Customer-facing project name. |
domain | string | Custom domain attached to the site. |
site_url | url | Public URL of the live site. |
published_at | ISO 8601 datetime | When the site went live. |
domain.purchasedFires when a domain purchase completes successfully.
| Field | Type | Description |
|---|---|---|
project_id | uuid | Project the domain was attached to. |
domain | string | Purchased domain name. |
purchased_at | ISO 8601 datetime | Purchase completion time. |
subscription.canceledFires when your subscription is canceled (by you or for non-payment).
| Field | Type | Description |
|---|---|---|
subscription_id | string | Stripe subscription identifier. |
canceled_at | ISO 8601 datetime | When the subscription was canceled. |
The full delivery body is { event, data, occurred_at, webhook_id } — your payload above lives under data. X-MM-Event echoes the event type and X-MM-Webhook-ID is unique per delivery so you can dedupe on your side.
We’re happy to talk about a deeper integration — custom webhooks, an SDK, or an API key for your own dashboard. Reach out and we’ll scope it together.