Theme
Forms + Submissions API
Design forms in the admin (contact, event signup, lead capture), collect submissions, export to CSV. Replaces the old mailto: waitlist stub with real stored submissions.
Admin: form definitions
http
GET /v1/spaces/{slug}/forms
POST /v1/spaces/{slug}/forms
GET /v1/spaces/{slug}/forms/{uuid}
PUT /v1/spaces/{slug}/forms/{uuid}
DELETE /v1/spaces/{slug}/forms/{uuid} # archives (soft) — submissions keptAuth: auth:sanctum + space.scope.
POST body:
json
{
"slug": "contact",
"label": "Kontaktformular",
"fields": [
{ "key": "name", "type": "text", "label": "Name", "required": true },
{ "key": "email", "type": "email", "label": "E-Mail", "required": true },
{ "key": "message", "type": "textarea", "label": "Nachricht", "required": false },
{ "key": "consent", "type": "consent", "label": "DSGVO-Einwilligung", "required": true }
],
"submit_action": "store",
"notification_email": "office@example.com",
"redirect_url": "https://example.com/danke",
"success_message": { "de": "Danke! Wir melden uns." },
"rate_limit_per_ip": 5
}Field type ∈ text · textarea · email · tel · number · select · radio · checkbox · file · hidden · consent.
submit_action:
| Value | Effect |
|---|---|
store (default) | Persist the submission only |
email | Persist + notify notification_email |
webhook | Persist + HMAC-signed POST to webhook_url |
lead | Persist + create/update a Subscriber |
On save, the field definitions are compiled into a JSON-Schema (compiled_schema) and validated server-side by the same ComponentSchemaValidator used for content blueprints — one validator for both surfaces.
Public: submit
http
POST /v1/cdn/{space}/forms/{slug}
Content-Type: application/jsonAnonymous. Throttled 20/min/IP at the route, plus the per-form rate_limit_per_ip (default 5/day/IP via a daily-rotating IP hash).
json
{
"_hp": "",
"data": { "name": "Lisa", "email": "lisa@example.com", "message": "Hallo" }
}_hpis the honeypot — a hidden field that must stay empty. If a bot fills it, the API returns a fake200 {ok:true}and stores nothing (the bot doesn't learn it was caught).datais validated against the form's compiled schema; missing required fields →422 {error:"validation_failed", errors:{…}}.- Values are coerced to scalars +
strip_tags+ length-capped (5000) on capture.
Success: 201 {data:{ok:true, submission_uuid, success_message, redirect_url}}.
Spam protection
| Layer | Mechanism |
|---|---|
| Honeypot | Built-in _hp field, always on |
| Rate limit | Route throttle + per-form rate_limit_per_ip (daily IP hash, no raw IP stored) |
| Challenge (optional) | spam_provider: turnstile | hcaptcha + spam_config |
Admin: submissions
http
GET /v1/spaces/{slug}/forms/{uuid}/submissions?per_page=50&include_spam=0
GET /v1/spaces/{slug}/forms/{uuid}/submissions.csv
POST /v1/spaces/{slug}/forms/{uuid}/submissions/{submissionUuid}/spam # toggleThe CSV export streams field-keyed columns (definition order) + meta columns (submitted_at, uuid, spam_score), excluding spam-marked rows. File fields are omitted from the CSV.
Privacy / DSGVO
- No raw IP stored — only a daily-rotating SHA-256 hash for rate-limiting + dupe detection (can't be reversed into a stable id).
- User-Agent stored as a 16-char hash, not the full string.
- Referer truncated to scheme + host.
- A
consent-type field records explicit opt-in; pair withsubmit_action: leadto feed double-opt-in newsletter flows.
Example: drop-in HTML form
html
<form id="contact">
<input name="name" required>
<input name="email" type="email" required>
<textarea name="message"></textarea>
<input name="_hp" style="display:none" tabindex="-1" autocomplete="off">
<button>Senden</button>
</form>
<script>
document.getElementById('contact').addEventListener('submit', async (e) => {
e.preventDefault();
const fd = new FormData(e.target);
const data = {}; fd.forEach((v, k) => { if (k !== '_hp') data[k] = v; });
const res = await fetch('https://peacock-cms.webhoch.com/v1/cdn/your-space/forms/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ _hp: fd.get('_hp') || '', data }),
});
const json = await res.json();
if (json.data?.ok) location.href = json.data.redirect_url || '/danke';
});
</script>See also
/api/analytics— newsletter subscribers (lead action target)/api/webhooks— webhook submit_action signing/guide/live-builder— edit form copy inline