The webhook
The top-level structure shared by every delivery, and the shared object shapes inside data.
Every event shares one top-level structure. Only event and data vary. Every id in the payload is a stable UUID that is safe to store and reference. All timestamps are ISO 8601 in UTC and end in Z.
We announce every payload change in advance. New fields are additive and never change the webhookVersion; build your integration to ignore fields it does not recognize, so additions never affect it. A field is only removed or renamed with a new webhookVersion, announced ahead of time with a migration period.
Attributes
iduuidUnique delivery id, stable across retries. Use it to deduplicate.
eventstringThe event type.
form_submissionform_stepcompleted_leadpartial_leadbooked_callwebhookVersionstringThe payload version this webhook is pinned to. You control it from the webhook's settings.
createdAtUtctimestampWhen the event happened, for example the moment the form was submitted. If a delivery is retried, this keeps its original value, so it is not the time the HTTP request was sent.
agencyobjectThe agency the subaccount belongs to.
+Show child attributes
iduuidnamestringsubaccountobjectThe subaccount that owns the webhook and the form.
+Show child attributes
iduuidnamestringformobjectThe form the event happened on.
+Show child attributes
iduuidnamestringurlstringdataobjectEvent-specific content. See the page for each event.
{
"id": "4b8e2f6a-9c3d-4e7b-a1f5-8d2c6b9e0a47",
"event": "completed_lead",
"webhookVersion": "2026-07-01",
"createdAtUtc": "2026-07-28T14:32:11.482Z",
"agency": {
"id": "c1a2b3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"name": "Acme Marketing"
},
"subaccount": {
"id": "7f3e9d21-4b8a-4c5e-9f60-2d8b1a7c3e54",
"name": "Acme Fitness Client"
},
"form": {
"id": "e8b4c672-9a1d-4f3b-b7c8-5d2e9f0a1b3c",
"name": "Fitness Coaching Application",
"url": "https://forms.roasform.com/fitness-coaching"
},
"data": {}
}Request headers
| Header | Example | Meaning |
|---|---|---|
Content-Type | application/json | Body is JSON. |
User-Agent | ROASForm-Webhooks/1 | Identifies ROASForm. |
X-ROASForm-Event | completed_lead | The event type. |
X-ROASForm-Delivery | 4b8e2f6a-… | Same as the webhook's id. |
X-ROASForm-Attempt | 2 | 1 on the first try, higher on retries. |
X-ROASForm-Signature | t=1753712000,v1=5f8a… | HMAC signature. See Verifying signatures. |
Shared objects
These shapes appear inside data across events. A field that has no value is null, never omitted.
lead
The lead is the person, not the run: leads are matched across submissions by
email or phone within the subaccount. Contact fields captured by this submission
are merged into the saved record before delivery, so the object arrives as
complete as we know the person — a returning visitor's company from an earlier
form shows up even if this form never asked for it, tags accumulate across
runs, and createdAtUtc is when the person was first seen. For what this
specific run captured, read submission.answers.
iduuidnamenullable stringemailnullable stringphonenullable stringcompanynullable stringlocationnullable stringtagsstring[]timezonenullable stringAmerica/Chicago.countrynullable stringcreatedAtUtctimestamp{
"id": "3d9f7a25-6c1b-4e8d-a2f4-8b0c5d7e9f13",
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "+15555550100",
"company": "Doe Fitness LLC",
"location": "Austin, TX",
"tags": ["hot-lead"],
"timezone": "America/Chicago",
"country": "US",
"createdAtUtc": "2026-07-28T14:29:03.114Z"
}submission
iduuidstatusstringpartialcompletequalificationstringThe qualification outcome of this submission, set by the ending reached;
none while partial.
nonequalifiedunqualifiedscorenumberstartedAtUtctimestampcompletedAtUtcnullable timestampnull while partial.endingnullable objectWhich ending screen the visitor reached — the exit path's identity.
Screen content and redirect URLs are builder configuration and are not
delivered. null while partial. If the ending was later deleted in the
builder, id is null and name/type come from the values saved with
the submission — so route on type, match exact endings on id, and treat
id: null as an ending that no longer exists in the builder.
+Show child attributes
idnullable uuidnamestringtypestringcalendarredirectsuccesslastSeenElementnullable objectThe question the visitor was on most recently, in the same shape as an
answers entry. For abandoned submissions this is where they stopped;
value, values, and score are null when that question was never
answered.
answersarrayOne entry per answered question, in the order each question was first answered (the visitor's path through the form, which can differ from the builder order when logic jumps). An answer edited later keeps its original position. See the answer shape below.
appliedTagsstring[]Tags this submission applied.
utmDatanullable objectUTM parameters captured with the submission.
ipAddressnullable stringThe visitor's IP address.
userAgentnullable stringThe visitor's browser user agent.
answers[]
elementIduuidlabelstringtypestringThe question's element type. Forms created long ago may also send the legacy
types single_select and multi_select.
nameemailphoneaddresswebsitetextlong_textshort_textmultiple_choicesingle_choiceimage_choicedropdownyes_noopinion_scaleratingnumberdatefile_uploadvaluenullable stringSingle-value answers. null for multi-select answers.
valuesnullable string[]Multi-select answers. null otherwise.
scorenullable numbernull for unscored questions.{
"id": "5a2c8e94-1f7d-4b3a-9e6c-4d8f0b2a6c71",
"status": "complete",
"qualification": "qualified",
"score": 40,
"startedAtUtc": "2026-07-28T14:26:40.207Z",
"completedAtUtc": "2026-07-28T14:32:11.482Z",
"ending": {
"id": "9c4b7d13-8e2a-4f6c-b1d9-7a3e5c8f2d46",
"name": "Qualified - Book a call",
"type": "calendar"
},
"lastSeenElement": {
"elementId": "a1d4c7f2-9b3e-4c6a-8d2f-5e9b1c4a7d30",
"label": "What is your email?",
"type": "email",
"value": "jane@example.com",
"values": null,
"score": null
},
"answers": [
{
"elementId": "b7e2f9a4-3c8d-4a1e-9f5b-6d0c2e8a4f17",
"label": "What is your monthly budget?",
"type": "single_choice",
"value": "$2,000 - $5,000",
"values": null,
"score": 30
},
{
"elementId": "d2c8f5b1-7e4a-4d9c-a6f3-9b0e5d8c2a64",
"label": "What are your goals?",
"type": "multiple_choice",
"value": null,
"values": ["Lose weight", "Build muscle"],
"score": 10
}
],
"appliedTags": ["hot-lead"],
"utmData": {
"utm_source": "facebook",
"utm_medium": "cpc",
"utm_campaign": "summer-2026"
},
"ipAddress": "203.0.113.42",
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15"
}Data freshness
Deliveries are built by reading the database at send time, not from a frozen snapshot — so most payloads (delivered within seconds of the event) are effectively point-in-time, but a retried delivery reflects anything that changed in between. What that means per field:
| Frozen at the event | Live at delivery time |
|---|---|
id, createdAtUtc, webhookVersion | the whole lead object (current state of the person) |
submission.startedAtUtc / completedAtUtc, qualification, utmData, ipAddress, userAgent | form.name / form.url, agency and subaccount names |
| answer texts and values (saved as answered — later question renames don't rewrite them) | ending.name (renames show; deletion falls back to the saved snapshot) |
the ending's type (survives even deletion via the snapshot) | appointment (a reschedule before delivery shows the new time) |
On events for still-partial submissions (form_step, partial_lead), a retried
delivery can also reflect further progress the visitor made in the meantime —
treat every delivery as a fresh snapshot keyed by submission.id.
