Forms API
Every Forms action (7) and data model (3): endpoints, scopes, input, preconditions, events and examples.
Public forms anyone can fill in; answers arrive as leads or Base records.
Module code forms · 7 actions · 3 data models · 8 events. A key or token never does more here than the person behind it can; row-level security applies exactly as in the app. Scopes: forms (everything), forms:read (read only) or forms:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
form_submissions | form_submission | /api/v1/form_submissions | |||
public_form_fields | public_form_field | /api/v1/public_form_fields | |||
public_forms | public_form | /api/v1/public_forms |
Lists take limit (up to 200), cursor, order (e.g. -created_at), fields and q; every other query parameter is a column filter: ?status=paid, ?total[gte]=100, ?id[in]=a,b, ?x=null.
Actions
| Action | Needs | What it does |
|---|---|---|
forms.close_form | forms:manage | Stop a form answering |
forms.discard_submission | forms:manage | Discard a failed submission |
forms.export_submissions | forms:export | Export submissions |
forms.record_retry | forms:manage | Record a retried submission |
forms.save_form | forms:manage | Create or edit a form |
forms.set_fields | forms:manage | Set a form’s fields |
forms.submit | forms:write | Send a form from the website |
forms.close_form
Stop a form answering — The address keeps working and tells visitors the form is closed.
Needs forms:manage · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/public_forms/{id}/actions/close_form- Canonical address:
POST /api/v1/actions/forms.close_form
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Form · id of public_form |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
form_not_open | It must be answering to be closed. |
Events it publishes: forms.form_closed
What it writes: public_form (update) — Stops it answering
curl -X POST https://app.getera.io/api/v1/public_forms/PUBLIC_FORM_ID/actions/close_form \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"forms.form_closed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.publicForms.closeForm("PUBLIC_FORM_ID")era public_forms close_form PUBLIC_FORM_ID{
"name": "era__forms__close_form",
"arguments": {
"id": "PUBLIC_FORM_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold forms:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 form_not_open · 429 slow down
forms.discard_submission
Discard a failed submission — Keeps it in the log, out of the failed list. Nothing is created from it.
Needs forms:manage · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/form_submissions/{id}/actions/discard_submission- Canonical address:
POST /api/v1/actions/forms.discard_submission
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Submission · id of form_submission |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
submission_not_found | The submission must exist. |
submission_not_failed | Only a failed submission is discarded. |
Events it publishes: forms.submission_discarded
What it writes: form_submission (update) — Marks it discarded
curl -X POST https://app.getera.io/api/v1/form_submissions/FORM_SUBMISSION_ID/actions/discard_submission \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"forms.submission_discarded"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.formSubmissions.discardSubmission("FORM_SUBMISSION_ID")era form_submissions discard_submission FORM_SUBMISSION_ID{
"name": "era__forms__discard_submission",
"arguments": {
"id": "FORM_SUBMISSION_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold forms:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 submission_not_found, submission_not_failed · 429 slow down
forms.export_submissions
Export submissions — Every submission of a form as a table: answers by question, status, record and consent.
Needs forms:export · reaches outside (sends, publishes or charges)
https://app.getera.io/api/v1/actions/forms.export_submissionsInput
| Field | Type | Required | Notes |
|---|---|---|---|
formId | uuid | yes | Form · id of public_form |
status | string (enum) | Only · Allowed values: accepted, failed, spam, discarded |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
form_not_found | The form must exist. |
Events it publishes: forms.submissions_exported
curl -X POST https://app.getera.io/api/v1/actions/forms.export_submissions \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"formId":"PUBLIC_FORM_ID"}'{
"ok": true,
"result": "…",
"events": [
"forms.submissions_exported"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.actions.run("forms.export_submissions", {
formId: "PUBLIC_FORM_ID"
})era forms export_submissions \
--form-id "PUBLIC_FORM_ID"{
"name": "era__forms__export_submissions",
"arguments": {
"formId": "PUBLIC_FORM_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold forms:export · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 form_not_found · 429 slow down
forms.record_retry
Record a retried submission — Marks a failed submission accepted with the record it became, or failed again.
Needs forms:manage · changes data
https://app.getera.io/api/v1/form_submissions/{id}/actions/record_retry- Canonical address:
POST /api/v1/actions/forms.record_retry
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Submission · id of form_submission |
recordType | string | Record type · max 40 | |
recordId | uuid | Record | |
error | string | Why it failed · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
submission_not_found | The submission must exist. |
submission_not_failed | Only a failed submission is retried. |
Events it publishes: forms.submission_recovered, forms.submission_failed
What it writes: form_submission (update) — Writes the retry outcome
curl -X POST https://app.getera.io/api/v1/form_submissions/FORM_SUBMISSION_ID/actions/record_retry \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"recordType":"Record type"}'{
"ok": true,
"result": "…",
"events": [
"forms.submission_recovered",
"forms.submission_failed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.formSubmissions.recordRetry("FORM_SUBMISSION_ID", {
recordType: "Record type"
})era form_submissions record_retry FORM_SUBMISSION_ID \
--record-type "Record type"{
"name": "era__forms__record_retry",
"arguments": {
"id": "FORM_SUBMISSION_ID",
"recordType": "Record type"
}
}Errors: 401 no or expired credential · 403 the credential does not hold forms:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 submission_not_found, submission_not_failed · 429 slow down
forms.save_form
Create or edit a form — Names a public form, decides what it creates and whether it is answering.
Needs forms:manage · changes data
https://app.getera.io/api/v1/public_forms/{id}/actions/save_form- Canonical address:
POST /api/v1/actions/forms.save_form
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | Filled from the path {id} · Form · id of public_form | |
slug | string | yes | Address · max 80 |
title | string | yes | Title · max 200 |
kind | string | Creates · max 32 | |
dataTableId | uuid | Table · id of data_table | |
savedViewId | uuid | Form view · id of saved_view | |
description | string | Description | |
successMessage | string | Thank-you message | |
consentText | string | Consent the visitor must give · max 2000 | |
redirectUrl | string | Send them to · max 2000 | |
settings | object / array | Behaviour | |
isActive | boolean | Answering |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
invalid_slug | The public address must be a slug. |
slug_taken | Two forms may not answer on one address. |
unknown_form_kind | A form must create something this module knows. |
target_module_off | A lead form needs Sales switched on. |
form_table_not_found | A table form needs a Base table. |
view_not_form | A form is opened from a Form view of its own table. |
invalid_redirect | A redirect is an http or https address. |
Events it publishes: forms.form_saved
What it writes: public_form (upsert) — Writes the form
curl -X POST https://app.getera.io/api/v1/public_forms/PUBLIC_FORM_ID/actions/save_form \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"slug":"Address","title":"Title"}'{
"ok": true,
"result": "…",
"events": [
"forms.form_saved"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.publicForms.saveForm("PUBLIC_FORM_ID", {
slug: "Address",
title: "Title"
})era public_forms save_form PUBLIC_FORM_ID \
--slug "Address" \
--title "Title"{
"name": "era__forms__save_form",
"arguments": {
"id": "PUBLIC_FORM_ID",
"slug": "Address",
"title": "Title"
}
}Errors: 401 no or expired credential · 403 the credential does not hold forms:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 invalid_slug, slug_taken, unknown_form_kind, target_module_off, form_table_not_found, view_not_form, invalid_redirect · 429 slow down
forms.set_fields
Set a form’s fields — Replaces the whole field list, in the order they will be shown, with their rules.
Needs forms:manage · removes or cannot be undone
https://app.getera.io/api/v1/public_form_fields/actions/set_fields- Canonical address:
POST /api/v1/actions/forms.set_fields
Input
| Field | Type | Required | Notes |
|---|---|---|---|
formId | uuid | yes | Form · id of public_form |
fields | object / array | yes | Fields |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
form_not_found | The form must exist. |
invalid_fields | Every field needs a label and a target. |
duplicate_target | Two fields may not write the same thing. |
unknown_target | A field must write something that exists. |
type_not_submittable | A public form cannot ask a stranger for a file, a reference or raw JSON. |
invalid_visibility | A field is shown depending only on questions above it. |
invalid_validation | Lengths and bounds are numbers, a pattern is a regular expression. |
Events it publishes: forms.fields_changed
What it writes: public_form_field (delete) — Clears the old list; public_form_field (insert) — Writes the new one, in order
curl -X POST https://app.getera.io/api/v1/public_form_fields/actions/set_fields \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"formId":"PUBLIC_FORM_ID","fields":{}}'{
"ok": true,
"result": "…",
"events": [
"forms.fields_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.publicFormFields.setFields({
formId: "PUBLIC_FORM_ID",
fields: {}
})era public_form_fields set_fields --data '{"formId":"PUBLIC_FORM_ID","fields":{}}'{
"name": "era__forms__set_fields",
"arguments": {
"formId": "PUBLIC_FORM_ID",
"fields": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold forms:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 form_not_found, invalid_fields, duplicate_target, unknown_target, type_not_submittable, invalid_visibility, invalid_validation · 429 slow down
forms.submit
Send a form from the website — A visitor fills in one of your forms on your site.
Needs forms:write · reaches outside (sends, publishes or charges) · Also callable from a published site (visitor)
/api/site/v1/{siteId}, e.g. with @getera/storefront). With an API key or a token it is refused (403 site_only).https://app.getera.io/api/v1/form_submissions/actions/submit- Canonical address:
POST /api/v1/actions/forms.submit
Input
| Field | Type | Required | Notes |
|---|---|---|---|
slug | string | yes | Form · max 80 |
answers | object / array | Answers | |
meta | object / array | Form details |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
form_not_answering | The form must be open and belong to this site’s business. |
form_writes_table | A table form is written by its own door. |
answers_invalid | Required and valid answers. |
consent_required | The consent box, when the form asks for it. |
too_many_submissions | One address may only send so often. |
Events it publishes: forms.submitted
What it writes: form_submission (insert) — Logs the answers; a lead form also creates its lead
Errors: 401 no or expired credential · 403 the credential does not hold forms:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 form_not_answering, form_writes_table, answers_invalid, consent_required, too_many_submissions · 429 slow down
Data models
FormSubmission
Table form_submission · Resource form_submissions
GET /api/v1/form_submissionsGET /api/v1/form_submissions/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
form_id | string (uuid) · uuid | public_form | ||
share_id | string (uuid) · uuid | yes | ||
status | string · text | accepted, failed, spam, discarded | ||
answers | object · jsonb | |||
record_type | string · text | yes | ||
record_id | string (uuid) · uuid | yes | ||
error | string · text | yes | ||
consent_at | string (date-time) · timestamptz | yes | ||
consent_text | string · text | yes | ||
attempts | integer · int4 | |||
resolved_by | string (uuid) · uuid | yes | app_user | |
resolved_at | string (date-time) · timestamptz | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((jsonb_typeof(answers) = 'object'::text))CHECK (((status <> 'accepted'::text) OR (record_id IS NOT NULL)))
PublicFormField
Table public_form_field · Resource public_form_fields
GET /api/v1/public_form_fieldsGET /api/v1/public_form_fields/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
form_id | string (uuid) · uuid | public_form | ||
label | string · text | |||
help | string · text | yes | ||
target | string · text | record, custom, column | ||
target_key | string · text | |||
is_required | boolean · bool | |||
col_span | integer · int2 | |||
section | string · text | yes | ||
display_order | integer · int4 | |||
created_at | string (date-time) · timestamptz | |||
placeholder | string · text | yes | ||
default_value | string · text | yes | ||
visible_when | object · jsonb | yes | ||
validation | object · jsonb |
Primary key: id
Unique: form_id, target, target_key
Checks:
CHECK ((length(btrim(label)) > 0))CHECK ((col_span = ANY (ARRAY[1, 2])))CHECK (((jsonb_typeof(validation) = 'object'::text) AND ((visible_when IS NULL) OR (jsonb_typeof(visible_when) = 'object'::text))))
PublicForm
Table public_form · Resource public_forms
GET /api/v1/public_formsGET /api/v1/public_forms/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
kind | string · text | lead, table | ||
slug | string · text | |||
title | string · text | |||
description | string · text | yes | ||
success_message | string · text | yes | ||
is_active | boolean · bool | |||
created_by | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
data_table_id | string (uuid) · uuid | yes | data_table | |
saved_view_id | string (uuid) · uuid | yes | ||
consent_text | string · text | yes | ||
redirect_url | string · text | yes | ||
settings | object · jsonb |
Primary key: id
Checks:
CHECK (((redirect_url IS NULL) OR (redirect_url ~ '^https?://'::text)))CHECK ((jsonb_typeof(settings) = 'object'::text))CHECK (((slug ~ '^[a-z0-9]+(-[a-z0-9]+)*$'::text) AND ((length(slug) >= 1) AND (length(slug) <= 80))))CHECK (((kind = 'table'::text) = (data_table_id IS NOT NULL)))
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
forms.fields_changed | A form’s field list was replaced. | id, count |
forms.form_closed | A public form stopped answering. | id |
forms.form_saved | A public form was created or edited. | id, slug, isActive |
forms.submission_discarded | A failed submission was set aside. | id, formId |
forms.submission_failed | A retried submission was refused again. | id, formId |
forms.submission_recovered | A failed submission was retried and went in. | id, formId |
forms.submissions_exported | Submissions were taken out of the platform. | formId, count |
forms.submitted | A stranger filled a form in and a record was created. | formId, slug, recordId |