Forms API
Build a form in the dashboard, then drive everything from your backend: submit entries, list and read submissions, manage form definitions, and export to CSV. All endpoints live under /api/v1/services/forms and are tenant-scoped - your sk_* key routes the request to your workspace (a sk_test_* key routes to the paired sandbox). Responses are wrapped in { "data": … }; lists add a meta block with limit, offset and has_more.
Scopes
| Scope | Allows |
|---|---|
service.forms.view | List forms, read a form definition, list + read submissions. |
service.forms.manage | Create, update and delete forms. |
service.forms.submissions.manage | Mark submissions read/unread, delete submissions. |
service.forms.export | Download submissions as CSV. |
service.forms.submit | POST a submission to a published form. |
Submit (server-to-server)
POST /api/v1/services/forms/forms/{slug}/submit · scope service.forms.submit
Accepts JSON or multipart/form-data (use multipart when the form has file fields). Field values can be sent flat or nested under data.
curl -X POST https://app.softsolz.uk/api/v1/services/forms/forms/contact-us/submit \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{ "full_name": "Jane Doe", "email": "jane@example.com", "message": "Hi" }'
Response 201:
{ "data": { "id": "42", "created_at": "2026-05-13T17:43:12Z",
"success_message": "Thanks - we've received your submission." } }
Response fields (inside data):
| Field | Type | Description |
|---|---|---|
id | integer | null | Created submission id. null when a honeypot trap silently drops the submission. |
created_at | string (date-time) | Submission timestamp. |
success_message | string | null | The form's configured success message, or null. |
Submit (widget)
POST /api/services/forms/widget/{slug}/submit · auth: X-Softsolz-Public-Key: pk_…
Used by our iframe loader from the browser. Accepts multipart/form-data so file fields work. Origin is checked against the key's allowlist; iframe-internal calls bypass the allowlist (they were already gated at iframe-load time). For server code, prefer the sk_* endpoint above.
List forms
GET /api/v1/services/forms/forms?limit=50&offset=0&status=published&search= · scope service.forms.view
Returns each form with field_schema, settings, appearance, submissions_count, unread_count, last_submission_at and source_counts.
Get a form
By id: GET /api/v1/services/forms/forms/{id}
By slug (the published definition you would render): GET /api/v1/services/forms/forms/by-slug/{slug}
Both require service.forms.view and return { "data": { …form… } }.
Create / edit / delete a form
Scope service.forms.manage.
POST /api/v1/services/forms/forms
PATCH /api/v1/services/forms/forms/{id}
DELETE /api/v1/services/forms/forms/{id}
curl -X POST https://app.softsolz.uk/api/v1/services/forms/forms \
-H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
-d '{ "name": "Contact us", "slug": "contact-us", "status": "published",
"field_schema": [
{ "id": "full_name", "type": "text", "label": "Full name", "required": true },
{ "id": "email", "type": "email", "label": "Email", "required": true },
{ "id": "message", "type": "textarea", "label": "Message" }
] }'
Request body fields (POST / PATCH):
| Field | Type | Required | Description |
|---|---|---|---|
name | string | required (POST) | Form name (trimmed). |
slug | string | optional | URL-safe slug, unique per tenant. Defaults to a slugified name. |
description | string | null | optional | Optional description (max 1000 chars). |
status | string | optional | One of draft, published, paused. Only published forms accept submissions. |
field_schema | array | optional | Field definitions: { id, type, label, required?, width?, options?, accept?, max_size_mb? }. |
settings | object | optional | success_message, redirect_url, notify_in_app, honeypot_enabled, max_submissions_per_day, etc. |
appearance | object | optional | accent_color, theme, font_family, button_style, etc. |
expected_updated_at | string (date-time) | optional (PATCH) | Optimistic-concurrency guard; a stale value returns 409. |
PATCH is a partial update; field_schema/settings/appearance are fully replaced when present. DELETE returns 204.
List submissions
GET /api/v1/services/forms/forms/{id}/submissions?search=&date_from=&date_to=&unread_only=true&limit=50&offset=0 · scope service.forms.view
Returns { "data": [ … ], "meta": { "limit", "offset", "total", "has_more" } }.
Get submission
GET /api/v1/services/forms/forms/{id}/submissions/{subId} · scope service.forms.view
Returns the submission with its field schema. Fetching marks it read. File fields include a 10-minute signed download_url plus a same-origin auth'd content_url for fetching as a Blob.
Mark read / delete a submission
Scope service.forms.submissions.manage.
PATCH /api/v1/services/forms/forms/{id}/submissions/{subId} { "read": false }
DELETE /api/v1/services/forms/forms/{id}/submissions/{subId}
Export submissions (CSV)
GET /api/v1/services/forms/forms/{id}/submissions.csv · scope service.forms.export
Streams a CSV with one column per form field (file fields export the signed download URL). Returns text/csv with a Content-Disposition: attachment header.
Webhook events
| Event | When |
|---|---|
forms.submission_received | Visitor or API submits a new entry. |
See Webhooks for payload + signature format.