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://mantelmarketing.com/api/health'
  • GET /api/domains/search

    Look up a domain

    GET /api/domains/searchbash
    curl 'https://mantelmarketing.com/api/domains/search?domain=acmebakery.com'
  • POST /api/contact

    Submit a contact form

    POST /api/contactbash
    curl -X POST 'https://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://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.

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.

POST/api/contact

Send 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
POST /api/contactbash
curl -X POST 'https://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": ""
  }'
Request bodyjson
{
  "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
Response bodyjson
{
  "ok": true,
  "data": {
    "sent": true
  }
}
POST/api/status/subscribe

Subscribe 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
POST /api/status/subscribebash
curl -X POST 'https://mantelmarketing.com/api/status/subscribe' \
  -H 'content-type: application/json' \
  -d '{ "email": "ops@acmebakery.com" }'
Request bodyjson
{
  "email": "ops@acmebakery.com"
}
Example response200 Confirmation email queued
Response bodyjson
{
  "ok": true,
  "data": {
    "ok": true
  }
}
GET/api/status/confirm

Confirm 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
NameInRequiredDescription
tokenstringqueryrequiredPer-row unsubscribe token issued at subscribe time
Example request
GET /api/status/confirmbash
curl -i 'https://mantelmarketing.com/api/status/confirm?token=YOUR_TOKEN'
Example response302 Redirect to /status?subscribed=1

No body — just the status code.

GET/api/status/unsubscribe

One-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
NameInRequiredDescription
tokenstringqueryrequired
Example request
GET /api/status/unsubscribebash
curl -i 'https://mantelmarketing.com/api/status/unsubscribe?token=YOUR_TOKEN'
Example response302 Redirect to /status?unsubscribed=1

No body — just the status code.

GET/api/health

Readiness 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
GET /api/healthbash
curl 'https://mantelmarketing.com/api/health'
Example response200 Healthy
Response bodyjson
{
  "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"
}
GET/changelog/rss.xml

RSS 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
GET /changelog/rss.xmlbash
curl -H 'accept: application/rss+xml' 'https://mantelmarketing.com/changelog/rss.xml'
Example response200 RSS 2.0 feed

No body — just the status code.

GET/changelog/atom.xml

Atom 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
GET /changelog/atom.xmlbash
curl -H 'accept: application/atom+xml' 'https://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.

FieldTypeDescription
project_iduuidProject identifier.
project_namestringCustomer-facing project name.
review_urlurlDeep link to the review page.
project.approved

Fires when you approve a site preview.

FieldTypeDescription
project_iduuidProject identifier.
project_namestringCustomer-facing project name.
project.revisions_requested

Fires when you request changes on a site preview.

FieldTypeDescription
project_iduuidProject identifier.
project_namestringCustomer-facing project name.
messagestringYour revision-request message.
project.published

Fires when your site goes live.

FieldTypeDescription
project_iduuidProject identifier.
project_namestringCustomer-facing project name.
domainstringCustom domain attached to the site.
site_urlurlPublic URL of the live site.
published_atISO 8601 datetimeWhen the site went live.
domain.purchased

Fires when a domain purchase completes successfully.

FieldTypeDescription
project_iduuidProject the domain was attached to.
domainstringPurchased domain name.
purchased_atISO 8601 datetimePurchase completion time.
subscription.canceled

Fires when your subscription is canceled (by you or for non-payment).

FieldTypeDescription
subscription_idstringStripe subscription identifier.
canceled_atISO 8601 datetimeWhen 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.

Talk to us  →Download the spec