> ## Documentation Index
> Fetch the complete documentation index at: https://docs.custral.com/llms.txt
> Use this file to discover all available pages before exploring further.

# form.submitted

> Somebody completed a form. Carries which form, which record it created, and how they reached it.

A form submission **is** a record: Custral has no separate submissions table, so
completing a form creates a record on the form's object. This event tells you
which form was completed and which record it produced.

## Payload

```json theme={null}
{
  "id": "whd_9Lm4tV2xB",
  "event": "form.submitted",
  "createdAt": "2026-09-11T14:31:02.554Z",
  "data": {
    "formId": "frm_4Hb7nQ",
    "formName": "Request access",
    "objectId": "obj_2Zc7pL",
    "recordId": "rec_8Pd3mW1",
    "endingId": "end_2Kf9",
    "source": "link"
  }
}
```

<ResponseField name="formId" type="string" required>
  The form that was submitted. Always present.
</ResponseField>

<ResponseField name="formName" type="string">
  The form's name at the time of submission.
</ResponseField>

<ResponseField name="objectId" type="string">
  The object the submission wrote a record to.
</ResponseField>

<ResponseField name="recordId" type="string">
  The record the submission created.
</ResponseField>

<ResponseField name="endingId" type="string">
  The ending screen the respondent reached, when the form branches. `null` for
  an in-app submission, which has no ending.
</ResponseField>

<ResponseField name="source" type="string">
  How the form was reached.

  | Value   | Means                                                        |
  | ------- | ------------------------------------------------------------ |
  | `link`  | The public share link                                        |
  | `embed` | Embedded on a site, or in a chat widget                      |
  | `app`   | A form block inside Custral, submitted by a signed-in member |
</ResponseField>

## The answers are not in this payload

Deliberately. The submission's answers are the record's fields, so read them
from the record rather than from the event:

```ts theme={null}
custral.on("form.submitted", async (event) => {
  const {formId, recordId, objectId, source} = event.data;
  if (formId !== process.env.ACCESS_FORM_ID) return;

  const record = await custral.records.retrieve({
    object: String(objectId),
    id: String(recordId),
  });

  await notifySales({answers: record.fields, camefrom: source});
});
```

A submission also fires
[`record.created`](/dev/webhooks/events/record-created) for the same record,
and **that** event carries the fields inline. If you subscribe to both,
deduplicate on `recordId`.

## Worth knowing

* **Only a submission that created a record fires this.** A refused submission,
  a validation failure, or an abandoned form does not.
* **Every source fires it**, including in-app submissions from a form block.
* **Analytics are separate.** Views, starts and per-page drop-off are recorded
  for the form's own funnel and are not delivered as webhooks. See
  [Forms](/comms/forms/overview).


## Related topics

- [Forms](/dev/webhooks/events/forms.md)
- [record.created](/dev/webhooks/events/record-created.md)
- [Webhooks](/dev/webhooks/overview.md)
- [Records](/dev/webhooks/events/records.md)
- [Event catalog](/dev/webhooks/events/overview.md)
