Developer docs
MantelMarketing API
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.
Quick start
Three commands to get started
GET /api/health
Check site availability
GET /api/healthbash curl 'https://www.mantelmarketing.com/api/health'GET /api/domains/search
Look up a domain
GET /api/domains/searchbash curl 'https://www.mantelmarketing.com/api/domains/search?domain=acmebakery.com'POST /api/contact
Submit a contact form
POST /api/contactbash curl -X POST 'https://www.mantelmarketing.com/api/contact' \ -H 'content-type: application/json' \ -d '{ "name": "Alex Rivera", "email": "alex@acmebakery.com", "message": "Curious about your construction tier — happy to chat about a multi-location rollout.", "website": "" }'
Authentication
Most public endpoints need none
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 API tokens — Bearer auth on dashboard routes
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://www.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 forPOST /api/publish.messages— required forPOST /api/projects/{id}/messages.files— required forPOST /api/files/presign.domain— required forPOST /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.
Reference
Endpoints
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/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.
Example request
curl -X POST 'https://www.mantelmarketing.com/api/contact' \
-H 'content-type: application/json' \
-d '{
"name": "Alex Rivera",
"email": "alex@acmebakery.com",
"message": "Curious about your construction tier — happy to chat about a multi-location rollout.",
"website": ""
}'{
"name": "Alex Rivera",
"email": "alex@acmebakery.com",
"message": "Curious about your construction tier — happy to chat about a multi-location rollout.",
"website": ""
}Example response200 Submission accepted
{
"ok": true,
"data": {
"sent": true
}
}/api/status/subscribeSubscribe an email address to status-page incident notifications
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.
Example request
curl -X POST 'https://www.mantelmarketing.com/api/status/subscribe' \
-H 'content-type: application/json' \
-d '{ "email": "ops@acmebakery.com" }'{
"email": "ops@acmebakery.com"
}Example response200 Confirmation email queued
{
"ok": true,
"data": {
"ok": true
}
}/api/status/confirmConfirm a status-page subscription
Customers click this link from the confirmation email. Sets `confirmed_at = now()` on the matching subscriber row and 302-redirects to `/status?subscribed=1`.
Parameters
| Name | In | Required | Description |
|---|---|---|---|
| tokenstring | query | required | Per-row unsubscribe token issued at subscribe time |
Example request
curl -i 'https://www.mantelmarketing.com/api/status/confirm?token=YOUR_TOKEN'Example response302 Redirect to /status?subscribed=1
No body — just the status code.
/api/status/unsubscribeOne-click unsubscribe from status-page notifications
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.
Parameters
| Name | In | Required | Description |
|---|---|---|---|
| tokenstring | query | required | — |
Example request
curl -i 'https://www.mantelmarketing.com/api/status/unsubscribe?token=YOUR_TOKEN'Example response302 Redirect to /status?unsubscribed=1
No body — just the status code.
/api/healthReadiness probe — checks Supabase, Clerk, Stripe, R2, Resend, Cloudflare, Slack
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.
Example request
curl 'https://www.mantelmarketing.com/api/health'Example response200 Healthy
{
"status": "ok",
"checks": {
"supabase": {
"status": "ok",
"latency_ms": 42
},
"stripe": {
"status": "ok",
"latency_ms": 88
},
"clerk": {
"status": "ok",
"latency_ms": 51
}
},
"release": "abc1234",
"timestamp": "2026-05-04T18:00:00.000Z"
}/api/domains/searchCheck whether a domain is available via Cloudflare Registrar
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).
Parameters
| Name | In | Required | Description |
|---|---|---|---|
| domainstring | query | required | FQDN to check, e.g. `acmebakery.com` |
Example request
curl 'https://www.mantelmarketing.com/api/domains/search?domain=acmebakery.com'Example response200 Availability result
{
"ok": true,
"data": {
"available": true,
"price": 12.5,
"currency": "USD"
}
}/changelog/rss.xmlRSS 2.0 changelog feed
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.
Example request
curl -H 'accept: application/rss+xml' 'https://www.mantelmarketing.com/changelog/rss.xml'Example response200 RSS 2.0 feed
No body — just the status code.
/changelog/atom.xmlAtom 1.0 changelog feed
Same release stream as the RSS feed, encoded as Atom 1.0. Cache-control is `public, max-age=3600, s-maxage=3600`.
Example request
curl -H 'accept: application/atom+xml' 'https://www.mantelmarketing.com/changelog/atom.xml'Example response200 Atom 1.0 feed
No body — just the status code.
Outbound webhooks
Webhook payloads
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_review
Fires 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.approved
Fires when you approve a site preview.
| Field | Type | Description |
|---|---|---|
project_id | uuid | Project identifier. |
project_name | string | Customer-facing project name. |
project.revisions_requested
Fires 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.published
Fires 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.purchased
Fires 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.canceled
Fires 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.
Need more?
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.