Webhooks
Send every submission to an address of your own, as JSON, the moment it arrives.
One POST per response
Sent as soon as the answer is stored, without keeping the respondent waiting.
A stable JSON shape
Public type names and permanent field IDs, so renaming a question never breaks your code.
10 seconds, one attempt
Fillio waits ten seconds for your endpoint and does not try again afterwards.
Turning a webhook on
Open the form, go to the Settings tab and switch Webhook on. Paste the address that should receive the submissions; Fillio saves it a moment after you stop typing and confirms it on screen. Each form carries one address, and switching the toggle back off clears it.
The address has to be HTTPS
Fillio only calls addresses that begin with https://. An http:// address is accepted by the settings field but is never called, so a webhook that appears to be saved will silently deliver nothing.
Addresses that point inside a private network are refused as well: any host on 10.x, 127.x, 169.254.x, 172.16-31.x or 192.168.x, any IPv6 address, and any name that is localhost or ends in .local or .internal. Your endpoint has to be reachable from the public internet.
The request
A POST with a JSON body. Four headers travel with it.
| Header | Description |
|---|---|
| Content-Type: application/json | The body is always JSON. |
| User-Agent: Fillio Webhooks/1.0 | Identifies the caller as Fillio, which is useful when one endpoint listens to several services. |
| X-Fillio-Event: FORM_RESPONSE | The event name. FORM_RESPONSE is the only value Fillio sends today. |
| X-Fillio-Event-Id | A fresh UUID for this delivery, repeated as eventId in the body. |
The payload
An envelope describing the event, and a data object describing the response.
{
"eventId": "8f2c1d7a-4b09-4e5c-9a13-6d0f8b2e77c4",
"eventType": "FORM_RESPONSE",
"createdAt": "2026-08-26T09:14:22.481Z",
"data": {
"responseId": "Qh3Rk9wZm1sVtY7cN2xP",
"formId": "7bTn4Kq2Wx",
"formName": "Workshop signup",
"createdAt": "2026-08-26T09:14:22.481Z",
"fields": [
{
"key": "V1StGXR8_Z5jdHi6B-myT",
"label": "Full name",
"type": "INPUT_TEXT",
"value": "Mira Kaya"
},
{
"key": "kM4pQz7LxAe2Rn0BsTvUw",
"label": "Which session?",
"type": "MULTIPLE_CHOICE",
"value": "c9XbNf3JdQ",
"options": [
{ "id": "c9XbNf3JdQ", "text": "Morning" },
{ "id": "eR2mYt8KpL", "text": "Afternoon" }
]
},
{
"key": "zA6cWn1FyH4tXd9QrLbEs",
"label": "How likely are you to recommend us?",
"type": "LINEAR_SCALE",
"value": 9
}
]
}
}| Property | Description |
|---|---|
| eventId | A unique ID for this delivery. |
| eventType | Always FORM_RESPONSE. |
| createdAt | When the response was submitted, as an ISO 8601 timestamp in UTC. |
| data.responseId | The ID of the stored response. |
| data.formId | The ID of the form, the same one that appears in its public link. |
| data.formName | The form title as plain text, with any formatting removed. |
| data.fields | One entry per answered question. |
Inside fields
Every entry carries four properties, and a question with options carries a fifth.
| Property | Description |
|---|---|
| key | The question's permanent ID. It survives every edit to the wording, so match on this rather than on the label. |
| label | The question as written in the editor. |
| type | The question type, taken from the list below. |
| value | The answer, in the shape that question stores. |
| options | Only on questions with options: every option's ID and text, so you can turn an answer back into a label. |
Only answered questions arrive
A question the respondent left untouched is absent from the array rather than sent as an empty value, and a question hidden by conditional logic never appears at all. Look each answer up by its key instead of relying on position or on a fixed number of entries.
Choice answers carry IDs, not labels
A dropdown or multiple choice answer is the chosen option's ID, and a checkbox, multi-select or ranking answer is an array of option IDs. Resolve them against the options list on the same entry. A ranking answer keeps the order the respondent put the items in.
The Other option
When a respondent writes into an Other box, the value is not an option ID but the text __OTHER__: followed by what they typed. Strip that prefix to read the answer.
"value": "__OTHER__:Heard about it from a friend"Files and signatures
A file upload answer is a download link to the file in Fillio's storage. A signature answer is a base64 PNG data URL, which you can save or display directly.
Field types
The type is a stable public name, so a change inside Fillio never reaches your integration. Anything Fillio cannot map arrives as INPUT_TEXT.
| Type | Question | Value |
|---|---|---|
| INPUT_TEXT | Short Answer | Text |
| TEXTAREA | Long Answer | Text |
| INPUT_EMAIL | Text | |
| INPUT_PHONE_NUMBER | Phone Number | Text |
| INPUT_NUMBER | Number | The number as text, exactly as it was typed |
| INPUT_LINK | Link | Text |
| INPUT_DATE | Date | A date, as YYYY-MM-DD |
| INPUT_TIME | Time | A time, as HH:MM on a 24-hour clock |
| DROPDOWN | Dropdown | One option ID |
| MULTIPLE_CHOICE | Radio | One option ID |
| CHECKBOXES | Checkbox | An array of option IDs |
| MULTI_SELECT | Multi-Select | An array of option IDs |
| RATING | Star Rating | A number |
| LINEAR_SCALE | Linear Scale | A number |
| RANKING | Ranking | An array of option IDs, in the respondent's order |
| FILE_UPLOAD | File Upload | A download link |
| SIGNATURE | Signature | A base64 PNG data URL |
If your endpoint does not answer
Fillio waits ten seconds. A timeout, a connection error or any status outside the 2xx range counts as a failed delivery, and that is the end of it: there is no retry and no queue to replay from.
The response itself is never affected. It is stored before the request is made and stays in your dashboard whatever your endpoint does, so the dashboard remains the complete record even if a delivery is lost.
There is no signature
Fillio does not sign the request and sends no shared secret, so the body on its own does not prove where it came from. If your endpoint has to be certain, give it an address only you know: a long, unguessable path or query string. Treat anything arriving elsewhere as untrusted.
Each delivery carries its own event ID, in both the X-Fillio-Event-Id header and the body. Recording the ones you have already handled is the simplest way to make your endpoint safe to call twice.
Point the webhook at any request-inspection endpoint and submit your own form once. You will see the exact payload your form produces, with your own field IDs in it. That is the fastest way to write the mapping on your side.