Skip to content

API Reference

The CloviForms API lets you integrate CloviForms into your own applications and automations. All endpoints are served over HTTPS from https://cloviforms.com.

Authentication

Authenticate every request with a Bearer token in the Authorization header. Generate a token from your account settings on the CloviForms dashboard.

curl https://cloviforms.com/api/me \
  -H "Authorization: Bearer $CLOVI_TOKEN"
import requests

resp = requests.get(
    "https://cloviforms.com/api/me",
    headers={"Authorization": f"Bearer {token}"},
)
resp.raise_for_status()
print(resp.json())
const resp = await fetch("https://cloviforms.com/api/me", {
  headers: { Authorization: `Bearer ${token}` },
});
const data = await resp.json();
console.log(data);
import axios from "axios";

const { data } = await axios.get(
  "https://cloviforms.com/api/me",
  { headers: { Authorization: `Bearer ${token}` } }
);
console.log(data);

Keep your token secret

Treat your API token like a password. Send it only over HTTPS and never commit it to source control — load it from an environment variable instead.

Responses & errors

All responses are JSON. Successful calls return 2xx; client errors return 4xx with a JSON body describing the problem. Common status codes:

Status Meaning
200 Success
400 Bad request — check your parameters
401 Missing or invalid token
404 Resource not found
429 Rate limit exceeded — slow down and retry
500 Server error — retry or contact support

CloviForms API Reference

Base URL: https://cloviforms.com (local dev: http://localhost:8975)
API prefix: /api/v1/
Content-Type: application/json (all requests and responses)
Auth tokens: JWT Bearer (Authorization: Bearer <token>). Token is returned on register/login. A per-user API key (cf_...) is also issued but is not yet accepted as a Bearer substitute on protected routes — the JWT token is required.
Errors: All errors return JSON {"error":"message"}. Common codes: 400 validation, 401 unauthorized, 402 payment required, 404 not found, 409 conflict, 429 rate limited, 503 service not configured.


Auth notes

  • All application routes use JWT Bearer token auth, not session cookies. The cf_session cookie is set as a convenience alias for browser clients but the underlying credential is the same JWT.
  • There is no public developer API using separate API keys at this time. All routes listed below are application routes (internal web app). A public developer API is planned.
  • Two route prefixes are equivalent for auth: /api/v1/auth/* and /api/auth/* both resolve to the same router (CT-537 fleet contract alias).

Rate limits

Scope Window Limit
General /api/* 15 min 200 req/IP
POST /api/v1/auth/register and /login 1 min 5 req/IP
POST /api/v1/appsumo/redeem 15 min 10 req/IP
POST /appsumo/webhook 15 min 60 req/IP
POST /api/v1/forms/:id/submit 1 min 30 req/IP
POST /api/v1/forms/:id/smartfield/:type 1 min 20 req/IP
POST /api/v1/billing/webhook 15 min 60 req/IP

Application Routes

All routes below are web application routes secured by JWT Bearer token unless noted as "no auth". There is no public API key auth layer yet.


Utility

GET /health

Returns server and database status. No auth required.

Auth: None
Parameters: None
Example:

curl https://cloviforms.com/health
Response:
{
  "status": "ok",
  "service": "cloviforms",
  "version": "1.0.0",
  "port": 8975,
  "db": "connected",
  "env": "production"
}


GET /api/v1/public-config

Returns publishable configuration visible to the browser (Stripe publishable key and available OAuth providers).

Auth: None
Parameters: None
Example:

curl https://cloviforms.com/api/v1/public-config
Response:
{
  "stripe_publishable_key": "pk_live_...",
  "social": { "google": true }
}


GET /embed.js

Public widget loader script. Served with Access-Control-Allow-Origin: * so any host page can load it. Mounts a same-origin iframe.

Auth: None
Parameters: None


GET /f/:formId

Serves the public form renderer HTML page (form.html SPA). Accepts numeric id or slug. Query param ?embed=1 relaxes CSP frame-ancestor restrictions for embedding.

Auth: None
Parameters: - :formId (path, string) — numeric form id or slug - embed (query, boolean, optional) — set to 1 to allow cross-origin framing



Auth — /api/v1/auth

Also available under /api/auth (same router, fleet alias).


POST /api/v1/auth/register

Register a new user, or upsert an existing user via SSO. Disposable email domains are blocked.

Auth: None
Rate limit: 5 req/min/IP
Body parameters: - email (string, required) — valid non-disposable email address - password (string, required for new non-SSO accounts) — minimum 8 characters - name (string, optional) — display name - clovitek_id (string, optional) — CloviTek fleet SSO identifier; if provided, password is not required for new accounts

Example:

curl -X POST https://cloviforms.com/api/v1/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"Secret123!","name":"Your Name"}'
Response (201):
{
  "token": "<jwt>",
  "user": {
    "id": 1,
    "email": "[email protected]",
    "name": "Your Name",
    "plan": "free",
    "api_key": "cf_..."
  }
}
Errors: 400 (email required / invalid format / disposable / password too short), 500


POST /api/v1/auth/login

Authenticate with email and password and receive a JWT.

Auth: None
Rate limit: 5 req/min/IP
Body parameters: - email (string, required) - password (string, required)

Example:

curl -X POST https://cloviforms.com/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","password":"Secret123!"}'
Response (200):
{
  "token": "<jwt>",
  "user": {
    "id": 1,
    "email": "[email protected]",
    "plan": "free",
    "api_key": "cf_..."
  }
}
Errors: 400 (missing fields), 401 (invalid credentials or SSO-only account), 500


GET /api/v1/auth/me

Returns the authenticated user's profile, current-month usage, active form count, and plan limits.

Auth: Bearer token (required)
Parameters: None
Example:

curl https://cloviforms.com/api/v1/auth/me \
  -H "Authorization: Bearer $TOKEN"
Response (200):
{
  "user": {
    "id": 1,
    "email": "[email protected]",
    "name": "Your Name",
    "plan": "free",
    "api_key": "cf_...",
    "created_at": "2026-01-01T00:00:00.000Z"
  },
  "usage": [
    { "action": "submissions_month", "total": 42 }
  ],
  "limits": { "forms": 3, "submissions_month": 100 },
  "form_count": 2
}
Errors: 401, 404, 500


POST /api/v1/auth/rotate-key

Rotates the caller's cf_ API key. The old key is immediately invalidated.

Auth: Bearer token (required)
Parameters: None (no body needed)
Example:

curl -X POST https://cloviforms.com/api/v1/auth/rotate-key \
  -H "Authorization: Bearer $TOKEN"
Response (200):
{ "api_key": "cf_<new-key>" }
Errors: 401, 500


POST /api/v1/auth/logout

Clears the cf_session browser cookie. As JWTs are stateless, clients must also discard the token locally.

Auth: None (stateless clear)
Parameters: None
Example:

curl -X POST https://cloviforms.com/api/v1/auth/logout
Response (200):
{ "ok": true }



Forms — /api/v1/forms


GET /api/v1/forms

Returns all active forms owned by the authenticated user.

Auth: Bearer token (required)
Parameters: None
Example:

curl https://cloviforms.com/api/v1/forms \
  -H "Authorization: Bearer $TOKEN"
Response (200):
{
  "forms": [
    {
      "id": 7,
      "title": "Contact Us",
      "description": null,
      "schema_json": "[...]",
      "schema": [{ "id": "name", "label": "Name", "type": "text", "required": true }],
      "settings": {},
      "slug": "f7abc12",
      "is_active": 1,
      "submission_count": 3,
      "created_at": "2026-01-01T00:00:00.000Z",
      "updated_at": "2026-01-02T00:00:00.000Z"
    }
  ]
}
Errors: 401, 500


POST /api/v1/forms

Creates a new form. Gated by the user's plan forms limit (number of active forms allowed on their tier).

Auth: Bearer token (required)
Body parameters: - title (string, required) — form title, max 255 chars - description (string, optional) - schema (array, required) — non-empty array of field objects (see Field schema below) - settings (object, optional) — form appearance/behavior settings (see Settings below)

Field schema: Each field must have: - id (string, unique within the form) - label (string, required unless type is page_break or html) - type (string) — one of: text, email, tel, textarea, select, checkbox, radio, number, date, time, url, password, hidden, rating, file, section, heading, page_break, html, payment, social_login, ai_assist, ai_humanize, qr_generate, pdf_render, url_scan_seo, url_scan_a11y, url_scan_security, domain_search - required (boolean, optional) - options (array of strings, required for select, checkbox, radio) - conditional (object, optional) — { field: "<id>", op: "eq|neq|contains|gt|lt", value: "..." } — references must point to earlier fields only - smart (object, optional, for Smart Field types) — { trigger: "on_blur|on_button|on_submit", cost_cap_cents: 0-1000 }

Settings object (all optional): - theme"dark" or "light" - accent — hex color string (e.g. "#6366f1") - submitText — button label (max 500 chars) - successTitle — post-submission heading - successMessage — post-submission message - redirectUrl — URL to redirect after submission - social{ provider: "google", require: true/false } — gate the form behind Google sign-in - payment{ amount: <cents>, currency: "usd", label: "Payment" } — attach a Stripe payment to the form

Example:

curl -X POST https://cloviforms.com/api/v1/forms \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Contact Us",
    "schema": [
      { "id": "name", "label": "Name", "type": "text", "required": true },
      { "id": "email", "label": "Email", "type": "email", "required": true },
      { "id": "message", "label": "Message", "type": "textarea" }
    ]
  }'
Response (201):
{
  "form": {
    "id": 7,
    "title": "Contact Us",
    "schema_json": "[...]",
    "schema": [...],
    "settings": {},
    "slug": "f7abc12",
    "is_active": 1,
    "submission_count": 0,
    "created_at": "2026-01-01T00:00:00.000Z"
  }
}
Errors: 400 (title required / schema invalid), 401, 402 (plan limit reached), 500


GET /api/v1/forms/:id

Returns a single form. Only the owner can access it. Accepts numeric id only (use /public for slug support without auth).

Auth: Bearer token (required)
Parameters: - :id (path, integer) — form id

Example:

curl https://cloviforms.com/api/v1/forms/7 \
  -H "Authorization: Bearer $TOKEN"
Response (200):
{ "form": { "id": 7, "title": "Contact Us", "schema": [...], "settings": {}, ... } }
Errors: 401, 404, 500


PUT /api/v1/forms/:id

Updates a form. Only the owner can update it. All body fields are optional; at least one must be provided.

Auth: Bearer token (required)
Parameters: - :id (path, integer) — form id

Body parameters (all optional, at least one required): - title (string) - description (string) - schema (array) — full replacement of the field schema; validated same as POST - settings (object) — full replacement of settings

Example:

curl -X PUT https://cloviforms.com/api/v1/forms/7 \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"title": "Updated Title"}'
Response (200):
{ "form": { "id": 7, "title": "Updated Title", ... } }
Errors: 400 (nothing to update / invalid schema), 401, 404, 500


DELETE /api/v1/forms/:id

Soft-deletes a form (sets is_active=0). The form data and submissions are retained in the database. Only the owner can delete.

Auth: Bearer token (required)
Parameters: - :id (path, integer) — form id

Example:

curl -X DELETE https://cloviforms.com/api/v1/forms/7 \
  -H "Authorization: Bearer $TOKEN"
Response (200):
{ "ok": true }
Errors: 401, 404, 500


GET /api/v1/forms/:id/public

Returns the public-facing form schema for rendering. No auth required. Accepts numeric id or slug.

Auth: None
Parameters: - :id (path, string) — numeric form id or slug

Example:

curl https://cloviforms.com/api/v1/forms/f7abc12/public
Response (200):
{
  "form": {
    "id": 7,
    "title": "Contact Us",
    "description": null,
    "schema_json": "[...]",
    "schema": [...],
    "settings": {},
    "slug": "f7abc12"
  }
}
Errors: 404 (not found or inactive), 500


POST /api/v1/forms/:id/submit

Submits data to a public form. No auth required. Accepts numeric id or slug.

Submission rules: - Fields must be keyed by their id (not label). Unrecognized keys are dropped. - Required visible fields must have values. Fields hidden by conditional logic are not validated or stored. - Honeypot fields (_hp, _honeypot, website, url_hp) must be empty; filled honeypot returns 200 silently without storing. - If the form has a payment field, _payment_intent (Stripe PaymentIntent id) must be provided and verified server-side against Stripe. - If the form has settings.social.require: true, _identity (JSON string from OAuth callback) must include a verified email. - File fields (type: "file", submitted as base64 data URL) are scanned by CloviGuard (ClamAV) before persistence. A blocked verdict rejects the whole submission. - Smart field results can be attached via _smart: { "<fieldId>": { data, render, provider, cost_cents } }.

Auth: None
Rate limit: 30 req/min/IP
Parameters: - :id (path, string) — numeric form id or slug

Body: JSON object with field ids as keys.

Example:

curl -X POST https://cloviforms.com/api/v1/forms/7/submit \
  -H 'Content-Type: application/json' \
  -d '{"name":"Jane Smith","email":"[email protected]","message":"Hello!"}'
Response (200):
{ "ok": true, "message": "Submission received. Thank you!" }
Errors: - 400 — required fields missing, or no valid fields submitted - 401 — social sign-in required (social_required: true) - 402 — payment required / not completed / mismatch (payment_required: true) - 404 — form not found or inactive - 422 — file blocked by security scan (security_blocked: true, field, scan details included) - 503 — upload security scan unavailable (security_unavailable: true) - 500


POST /api/v1/forms/:id/payment-intent

Creates a Stripe PaymentIntent for a form's payment field and returns the client_secret for the Stripe Payment Element. No auth required. The amount is determined entirely from the form's server-side settings; the client cannot set the price.

Auth: None
Parameters: - :id (path, string) — numeric form id or slug

Body: None required
Example:

curl -X POST https://cloviforms.com/api/v1/forms/7/payment-intent
Response (200):
{
  "client_secret": "pi_..._secret_...",
  "publishable_key": "pk_live_...",
  "amount": 4900,
  "currency": "usd",
  "payment_intent": "pi_..."
}
Errors: 400 (no payment field on form / no amount configured), 404, 503 (Stripe not configured), 502


GET /api/v1/forms/:id/submissions

Lists all submissions for a form. Only the owner can view. Paginated (newest first).

Auth: Bearer token (required)
Parameters: - :id (path, integer) — form id - page (query, integer, default 1) - limit (query, integer, default 50, max 100)

Example:

curl "https://cloviforms.com/api/v1/forms/7/submissions?page=1&limit=50" \
  -H "Authorization: Bearer $TOKEN"
Response (200):
{
  "form": { "id": 7, "title": "Contact Us" },
  "total": 42,
  "page": 1,
  "limit": 50,
  "submissions": [
    {
      "id": 101,
      "data_json": "{\"name\":\"Jane\",\"email\":\"[email protected]\"}",
      "submitter_ip": "1.2.3.4",
      "user_agent": "Mozilla/5.0 ...",
      "created_at": "2026-01-01T12:00:00.000Z"
    }
  ]
}
Errors: 401, 404, 500


GET /api/v1/forms/ai/usage

Returns the authenticated user's remaining AI generation credits for the current billing period.

Auth: Bearer token (required)
Parameters: None
Example:

curl https://cloviforms.com/api/v1/forms/ai/usage \
  -H "Authorization: Bearer $TOKEN"
Response (200): Shape from aiCredits.usage():
{ "remaining": 8, "used": 2, "limit": 10 }
Errors: 401


POST /api/v1/forms/ai/generate

Generates a form schema from a natural-language prompt using Claude (Anthropic). Gated by per-user AI credits based on plan tier.

Auth: Bearer token (required)
Body parameters: - prompt (string, required) — natural-language description of the form (max 2000 chars)

Example:

curl -X POST https://cloviforms.com/api/v1/forms/ai/generate \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"prompt":"A job application form with name, email, resume upload, and cover letter"}'
Response (200):
{
  "schema": [
    { "id": "name", "type": "text", "label": "Full Name", "required": true },
    { "id": "email", "type": "email", "label": "Email", "required": true }
  ],
  "remaining": 7
}
Errors: 400 (prompt required), 401, 402 (AI credit limit reached), 503 (AI not configured), 502 (AI provider error), 500


POST /api/v1/forms/:id/smartfield/:type

Server-side proxy for Smart Fields. Used by both the public renderer (form fills) and the builder live preview. Fleet API keys are kept server-side and never exposed to the browser. For metered types, the form owner's AI credits are consumed (not the submitter's).

Auth: None (public form fill context; owner's plan meters cost)
Rate limit: 20 req/min/IP
Parameters: - :id (path, string) — numeric form id or slug - :type (path, string) — smart field type; must match a registered field in the form schema

Body parameters: - fieldId (string, required) — the field's id from the form schema - value (string) — input value to process (required for all types except pdf_render) - mode (string, optional) — provider-specific mode/instruction - html (string, optional) — pre-built HTML payload for pdf_render type only

Supported smart field types:

Type Description Metered
ai_assist AI text assistance via Claude Yes
ai_humanize AI text humanization Yes
qr_generate QR code generation No
pdf_render PDF rendering from HTML No
url_scan_seo SEO analysis of a URL No
url_scan_a11y Accessibility scan of a URL No
url_scan_security Security scan of a URL No
domain_search Domain availability lookup (RDAP, local) No

Example:

curl -X POST https://cloviforms.com/api/v1/forms/7/smartfield/ai_assist \
  -H 'Content-Type: application/json' \
  -d '{"fieldId":"bio","value":"I work in marketing and love data."}'
Response (200):
{
  "ok": true,
  "render": "inline",
  "data": "Enhanced version of your text...",
  "provider": "anthropic",
  "cost_cents": 1
}
Errors: - 400 — unknown smart type / fieldId not in form / field type mismatch / no value - 402 — AI credit limit or per-field cost_cap_cents exceeded - 404 — form not found or inactive - 429 — rate limit exceeded - 502 — provider returned an error - 503 — provider unreachable - 500



OAuth (Social Login on Forms) — /api/v1/oauth

Used to verify a form submitter's Google identity. This is not for account login — it produces an identity object submitted alongside a form to satisfy settings.social.require: true forms.


GET /api/v1/oauth/session

Returns an existing CloviTek fleet identity from the cl_session cookie (SSO session), so a form can skip the OAuth popup if the visitor is already signed in to the fleet.

Auth: None (reads cl_session cookie)
Parameters: None
Response (200):

{
  "identity": {
    "provider": "cl_session",
    "email": "[email protected]",
    "name": "User Name",
    "sub": "123",
    "verified_email": true
  }
}
Returns { "identity": null } if no valid session cookie is present.


GET /api/v1/oauth/start

Initiates Google OAuth2 authorization-code flow. Redirects the browser (typically in a popup) to the Google consent screen.

Auth: None
Query parameters: - provider (string, required) — must be "google" - form (string, optional) — form id, embedded in signed state for CSRF protection on callback

Example:

GET /api/v1/oauth/start?provider=google&form=7
→ 302 redirect to https://accounts.google.com/o/oauth2/v2/auth?...
Errors: 400 (unsupported provider), 503 (Google OAuth not configured on server)


GET /oauth/callback

OAuth redirect URI registered with Google. Exchanges the authorization code, fetches the Google user profile, and sends the identity back to the form's opener window via postMessage. The popup closes automatically after ~800 ms.

Auth: None (OAuth code from Google)
Query parameters: - code (string) — OAuth authorization code - state (string) — signed JWT state from /oauth/start (replay/CSRF guard) - error (string, optional) — set by Google on access denial

Response: HTML page that calls:

window.opener.postMessage({ type: "cloviforms:oauth", identity: { ... } }, "*")
Identity on success:
{ "provider": "google", "sub": "...", "email": "[email protected]", "name": "User Name", "verified_email": true }
On failure, identity is replaced with "error": "<message>".



Billing — /api/v1/billing


POST /api/v1/billing/checkout

Creates a Stripe Checkout Session (subscription mode) for the authenticated user. Returns the hosted checkout URL to redirect to.

Auth: Bearer token (required)
Body parameters: - plan (string, required) — one of: starter, pro, business, agency

Example:

curl -X POST https://cloviforms.com/api/v1/billing/checkout \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"plan":"pro"}'
Response (200):
{ "url": "https://checkout.stripe.com/pay/cs_...", "id": "cs_..." }
Errors: 400 (invalid plan), 401, 404, 503 (Stripe not configured), 502


GET /api/v1/billing/portal

Opens a Stripe Billing Portal session for self-service subscription management. Requires the user to have an existing Stripe customer record (i.e., must have subscribed before).

Auth: Bearer token (required)
Parameters: None
Example:

curl https://cloviforms.com/api/v1/billing/portal \
  -H "Authorization: Bearer $TOKEN"
Response (200):
{ "url": "https://billing.stripe.com/session/..." }
Errors: 400 (no billing account — subscribe first), 401, 503 (Stripe not configured), 502


POST /api/v1/billing/webhook

Stripe webhook endpoint. Receives raw application/json body (bypasses express.json) and verifies the Stripe signature using STRIPE_WEBHOOK_SECRET. This is the source of truth for plan entitlement — plans are only granted after Stripe confirms payment.

Auth: Stripe Stripe-Signature header (HMAC-SHA256)
Content-Type: application/json (raw bytes, not parsed)
Events handled: - checkout.session.completed — grant plan to subscribing user - invoice.paid — confirm ongoing subscription plan - customer.subscription.deleted — downgrade user to free

Response (200):

{ "received": true }
Errors: 400 (invalid signature), 503 (webhook not configured)


POST /api/v1/billing/cb/checkout

Creates a Chargebee hosted-page checkout session. Alternative billing path alongside Stripe checkout.

Auth: Bearer token (required)
Body parameters: - plan (string, required) — one of: starter, pro, business, agency - cycle (string, optional) — "monthly" (default) or "annual"

Example:

curl -X POST https://cloviforms.com/api/v1/billing/cb/checkout \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"plan":"pro","cycle":"annual"}'
Response (200):
{ "url": "https://...", "id": "cbhp_...", "gateway": "chargebee" }
Errors: 400 (invalid plan), 401, 404, 502


POST /api/v1/billing/cb/webhook

Chargebee webhook endpoint. Verified via clovi_chargebee.py Python client. Activates or deactivates a user's plan based on Chargebee subscription events.

Auth: Chargebee Basic Auth (verified server-side via Python client, not in-process)
Actions handled: - activate — grant plan to user - deactivate — revert user to free

Response (200):

{ "received": true }
Errors: 401 (unauthorized), 500



AppSumo — License Activation

Three activation paths for AppSumo lifetime deal customers. All paths ultimately call provisionLicense() internally.

Tier to plan mapping:

AppSumo Tier Plan
1 starter
2 pro
3 business
4 or 5 agency

POST /api/v1/appsumo/redeem

Manual license code redemption. Works for new users (creates an account) and logged-in users (links the license). If a valid Bearer token is supplied and the token's user already owns the license, returns success immediately.

Auth: Optional Bearer token
Rate limit: 10 req/15 min/IP
Body parameters: - code (string, required) — AppSumo license key - email (string, required for new accounts when no Bearer token provided) - password (string, required for new accounts when no Bearer token provided) — minimum 8 characters

Example:

curl -X POST https://cloviforms.com/api/v1/appsumo/redeem \
  -H 'Content-Type: application/json' \
  -d '{"code":"AS-XXXX-XXXX","email":"[email protected]","password":"Secret123!"}'
Response (200):
{
  "ok": true,
  "token": "<jwt>",
  "plan": "pro",
  "message": "AppSumo license activated — plan upgraded to pro"
}
Errors: - 400 — missing code / email / password / deactivated license - 401 — wrong password for existing account - 404 — license key not found - 409 — license already linked to a different account - 500


POST /appsumo/webhook

Receives license lifecycle events from AppSumo. HMAC-SHA256 verified. Replay attacks blocked by a 5-minute timestamp window (X-AppSumo-Timestamp) and an in-process seen-signature cache.

Auth: AppSumo HMAC (X-AppSumo-Signature + X-AppSumo-Timestamp request headers)
Rate limit: 60 req/15 min/IP
Body parameters: - event (string, required) — event type - license_key (string, required) - tier (integer, optional, default 1) - test (boolean, optional) — if true, event is logged and ignored

Events handled: - purchase — store license as inactive (no user yet) - activate — mark license active - upgrade / downgrade — update tier and plan - deactivate / refund / chargeback — revoke license, downgrade user to free - migrate — record migration

Example:

# (AppSumo sends this — signature generation shown for reference only)
curl -X POST https://cloviforms.com/appsumo/webhook \
  -H 'Content-Type: application/json' \
  -H 'X-AppSumo-Signature: <hmac>' \
  -H 'X-AppSumo-Timestamp: <epoch>' \
  -d '{"event":"activate","license_key":"AS-XXXX-XXXX","tier":2}'
Response (200):
{ "ok": true, "event": "activate" }
Errors: 400 (missing event or license_key), 401 (invalid/expired signature or duplicate), 503 (HMAC key not configured), 500


GET /appsumo/callback

AppSumo OAuth redirect URI. Exchanges the authorization code for a license key via AppSumo OpenID, fetches license detail (tier), provisions the account, and sets a cf_session cookie before redirecting to the dashboard.

Auth: AppSumo OAuth (code query param from AppSumo redirect)
Query parameters: - code (string, required) — AppSumo OAuth authorization code - email (string, optional) — email hint for account creation if no existing user is found

Response: - On success: 302 /dashboard.html?appsumo=activated&plan=<plan> with cf_session cookie set - If account cannot be resolved yet: 302 /appsumo/activate?license_key=...&tier=...&plan=... - On failure: 302 /register.html?error=appsumo_oauth_failed


GET /appsumo/activate

Serves an HTML activation page where AppSumo buyers complete account setup (email + password). The page client-side calls POST /api/v1/appsumo/redeem with the pre-filled license key.

Auth: None (public HTML page)
Query parameters: - license_key (string) — pre-filled license key - tier (integer) — AppSumo tier number - plan (string) — resolved plan name displayed to the user

Response: HTML page



Plan Tiers Summary

Plan Tier Note
free Default on signup
starter 1 AppSumo Tier 1 / Stripe subscription
pro 2 AppSumo Tier 2 / Stripe subscription
business 3 AppSumo Tier 3 / Stripe subscription
agency 4–5 AppSumo Tier 4 or 5 / Stripe subscription

Plan limits (max active forms, monthly submissions, AI credits) are enforced server-side via the planGate middleware and are returned in GET /api/v1/auth/me under the limits key.


Public Developer API

A public developer API accepting the cf_... API key (returned in the user object) is not yet implemented. All routes above require JWT Bearer token auth from the register/login flow. The cf_... key is issued per user and reserved for a future public API surface.