// Public API · v1

Connect your own website to Clay.

Keep your marketing site as it is — Webflow, WordPress, Framer, or hand-built — and let signup, contact, and trial forms flow straight into Clay. One API key per gym. Scoped. EU-hosted. No extra tools.

What you get.

The Clay app is your daily workspace (classes, members, payments). The Public API is the bridge to your own brand site. You keep working in the app — forms, pricing block and class schedule stay in sync.

No webhooks to design for the basics: one POST from your form lands the lead in Clay → Acquisition → Leads, with source, UTM data and consent captured.

  • Tenant-scoped

    Every key is bound to your organization. Wrong-tenant leakage is impossible by construction.

  • Per-key scopes

    Give the Webflow key only `lead:write`. Give the pricing-page key only `plans:read`. Compromise is never a total surrender.

  • Browser-safe

    Allow-list your website origin per key. CORS preflight is built in. No cookies, no sessions — just the Bearer key.

  • EU-only

    API runs on Vercel EU. Postgres in Frankfurt. Resend EU. GDPR by architecture.

Quickstart in 5 minutes.

  1. Step 1

    Issue an API key

    Sign in at cms.clayapp.nl/admin → Integrations → Tenant API keys → New. Pick the scopes (start with `lead:write`), add your website origin (e.g. https://watchmangym.com) and Save. The plaintext key is shown once — store it in 1Password.

  2. Step 2

    Smoke-test the key

    GET /tenant with scope `tenant:read` to confirm the key works. You'll get back your organization id and slug.

  3. Step 3

    Wire the form

    Drop the JS snippet below into your Webflow / WordPress / Framer page. Replace `clay_live_…` with your key. POST to /leads on submit.

  4. Step 4

    See the leads

    Open Clay admin → Acquisition → Leads. Every submission is there, with IP, user-agent, UTM data and the key it came from. Assign to a team member and convert into a member when you've spoken.

Endpoints.

POST/api/public/v1/leadsscope: lead:write

Capture a lead

Catch signup forms, trial requests, and contact widgets from your own site. Organization is derived server-side from the key — never include it in the body.

// curl — POST /api/public/v1/leads
curl -X POST https://clayapp.nl/api/public/v1/leads \
  -H "Authorization: Bearer clay_live_xxxxxxxxYourSecretHere" \
  -H "Content-Type: application/json" \
  -d '{
    "email":             "lara@example.com",
    "first_name":        "Lara",
    "last_name":         "Janssen",
    "phone":             "+31612345678",
    "source":            "trial",
    "source_detail":     "Webflow / signup-block",
    "interest":          "trial",
    "message":           "Would like to book a trial",
    "consent_marketing": true,
    "locale":            "en",
    "metadata":          { "utm_campaign": "spring-2026" }
  }'
// 200 OK
{
  "ok": true,
  "data": {
    "id": 1234,
    "status": "new",
    "createdAt": "2026-04-28T11:23:45.000Z"
  }
}
GET/api/public/v1/classesscope: classes:read

Read the schedule

Render your live class schedule on your marketing site — no manual sync. Only `active` classes for your tenant; trainer/location are summarised to name + city so you don't need to map internal IDs.

// curl — GET /api/public/v1/classes
curl "https://clayapp.nl/api/public/v1/classes?from=2026-05-01T00:00:00Z&limit=20" \
  -H "Authorization: Bearer clay_live_xxxxxxxxYourSecretHere"
GET/api/public/v1/membership-plansscope: plans:read

Read membership plans

Render your pricing block dynamically — change something once in Clay and the website is in sync. Includes `access_model` (credits / flat fee), `credits_per_period`, `billing_interval` and amount in EUR.

// curl — GET /api/public/v1/membership-plans
curl https://clayapp.nl/api/public/v1/membership-plans \
  -H "Authorization: Bearer clay_live_xxxxxxxxYourSecretHere"
GET/api/public/v1/tenantscope: tenant:read

Health check / introspection

Confirm which organization + key the request is scoped to. Useful for first-deploy smoke tests or a no-code tool that wants to know which shop is behind it.

CORS & browser usage.

Want to call the API directly from your website's JavaScript (no backend in between)? Add your site's origin to the key's allow-list. Without an allow-list the key only works server-to-server.

  • An origin is scheme + host (+ port) without a path: `https://watchmangym.com`, not `https://watchmangym.com/contact`.
  • Add multiple origins for staging: `https://watchmangym.webflow.io` and `https://watchmangym.com`.
  • Preflight (OPTIONS) is answered automatically — no extra config required.
  • Keep server-to-server keys in a separate key without an allow-list, and only store them in your backend env vars. Browser keys must have an allow-list.

Error codes.

All errors share the same envelope: `{ ok: false, error: { code, message, details? } }`. Code your client against `code` (machine-readable), not `message` (may change).

codestatusmeaning
missing_authorization401Header missing
invalid_key_format401Key isn't in `clay_live_…` shape
invalid_key401Key unknown or secret mismatch
key_revoked401Key was revoked in the admin
key_expired401Key passed its expiry date
origin_not_allowed403Browser origin not in allow-list
missing_scope403Key missing the required scope (see `details`)
validation_failed400Body fails the schema (see `details.fieldErrors`)
rate_limited429Too many requests — honour `Retry-After`
auth_unavailable503Transient outage on the auth layer — retry

Recipes.

Webflow / WordPress / Framer — vanilla JS

Drop this into a Custom Code embed. Works 1:1 in any no-code tool that exposes a form-submit hook.

// html / javascript
<form id="trial-form">
  <input name="email" type="email" required />
  <input name="first_name" />
  <input name="phone" />
  <button type="submit">Book a trial</button>
</form>

<script>
  const CLAY_KEY = "clay_live_xxxxxxxxYourSecretHere";

  document.querySelector("#trial-form").addEventListener("submit", async (e) => {
    e.preventDefault();
    const fd = new FormData(e.currentTarget);
    const res = await fetch("https://clayapp.nl/api/public/v1/leads", {
      method: "POST",
      headers: {
        "Authorization": "Bearer " + CLAY_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        email:        fd.get("email"),
        first_name:   fd.get("first_name"),
        phone:        fd.get("phone"),
        source:       "trial",
        source_detail: location.pathname,
        consent_marketing: true,
        locale:       "en",
        metadata: {
          utm_source:   new URL(location.href).searchParams.get("utm_source"),
          utm_campaign: new URL(location.href).searchParams.get("utm_campaign"),
        },
      }),
    });
    const json = await res.json();
    if (json.ok) {
      e.currentTarget.reset();
      alert("Thanks — we'll email you within one business day.");
    } else {
      alert("Oops: " + json.error.message);
    }
  });
</script>

Zapier — Webhook → Clay

Connect anything with a Zapier trigger (Typeform, Tally, Calendly cancellations) into Clay without writing a line of code.

// Zapier action — Webhooks by Zapier · POST
URL:     https://clayapp.nl/api/public/v1/leads
Method:  POST
Headers:
  Authorization: Bearer clay_live_xxxxxxxxYourSecretHere
  Content-Type:  application/json
Data:
  email:           {{trigger.email}}
  first_name:      {{trigger.first_name}}
  phone:           {{trigger.phone}}
  source:          zapier
  source_detail:   "Typeform: Open day signup"
  consent_marketing: true

Server-to-server — Node 20+

For backends that run their own logic before POSTing into Clay. Plan: rotate keys via env vars and monitor `last_used_at` in the admin.

// node
const res = await fetch("https://clayapp.nl/api/public/v1/leads", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.CLAY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ email, first_name: firstName, source: "website" }),
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`Clay ${error.code}: ${error.message}`);
}

Ready to connect?

Issue your first API key in 30 seconds — no credit card required. Works on every Clay plan.