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

ScopeAllows
service.forms.viewList forms, read a form definition, list + read submissions.
service.forms.manageCreate, update and delete forms.
service.forms.submissions.manageMark submissions read/unread, delete submissions.
service.forms.exportDownload submissions as CSV.
service.forms.submitPOST 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):

FieldTypeDescription
idinteger | nullCreated submission id. null when a honeypot trap silently drops the submission.
created_atstring (date-time)Submission timestamp.
success_messagestring | nullThe 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):

FieldTypeRequiredDescription
namestringrequired (POST)Form name (trimmed).
slugstringoptionalURL-safe slug, unique per tenant. Defaults to a slugified name.
descriptionstring | nulloptionalOptional description (max 1000 chars).
statusstringoptionalOne of draft, published, paused. Only published forms accept submissions.
field_schemaarrayoptionalField definitions: { id, type, label, required?, width?, options?, accept?, max_size_mb? }.
settingsobjectoptionalsuccess_message, redirect_url, notify_in_app, honeypot_enabled, max_submissions_per_day, etc.
appearanceobjectoptionalaccent_color, theme, font_family, button_style, etc.
expected_updated_atstring (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

EventWhen
forms.submission_receivedVisitor or API submits a new entry.

See Webhooks for payload + signature format.