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.
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_sessioncookie 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:
{
"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:
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"}'
{
"token": "<jwt>",
"user": {
"id": 1,
"email": "[email protected]",
"name": "Your Name",
"plan": "free",
"api_key": "cf_..."
}
}
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!"}'
{
"token": "<jwt>",
"user": {
"id": 1,
"email": "[email protected]",
"plan": "free",
"api_key": "cf_..."
}
}
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:
{
"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
}
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:
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:
Forms — /api/v1/forms¶
GET /api/v1/forms¶
Returns all active forms owned by the authenticated user.
Auth: Bearer token (required)
Parameters: None
Example:
{
"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"
}
]
}
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" }
]
}'
{
"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"
}
}
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:
Response (200): Errors: 401, 404, 500PUT /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"}'
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:
Response (200): Errors: 401, 404, 500GET /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:
Response (200):{
"form": {
"id": 7,
"title": "Contact Us",
"description": null,
"schema_json": "[...]",
"schema": [...],
"settings": {},
"slug": "f7abc12"
}
}
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!"}'
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:
{
"client_secret": "pi_..._secret_...",
"publishable_key": "pk_live_...",
"amount": 4900,
"currency": "usd",
"payment_intent": "pi_..."
}
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"
{
"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"
}
]
}
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:
aiCredits.usage():
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"}'
{
"schema": [
{ "id": "name", "type": "text", "label": "Full Name", "required": true },
{ "id": "email", "type": "email", "label": "Email", "required": true }
],
"remaining": 7
}
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."}'
{
"ok": true,
"render": "inline",
"data": "Enhanced version of your text...",
"provider": "anthropic",
"cost_cents": 1
}
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
}
}
{ "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?...
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:
Identity on success:{ "provider": "google", "sub": "...", "email": "[email protected]", "name": "User Name", "verified_email": true }
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"}'
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:
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):
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"}'
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):
Errors: 401 (unauthorized), 500AppSumo — 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!"}'
{
"ok": true,
"token": "<jwt>",
"plan": "pro",
"message": "AppSumo license activated — plan upgraded to pro"
}
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}'
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.