Send every form response to your own webhook endpoint
Point Upform at a URL you control and each submission arrives as a single JSON POST: the form, the submission, an ISO-8601 timestamp, the completion state, and one typed answer per question. No polling, no export, no automation tool in the middle. It runs on the free plan, which has no cap on responses.
The short answer
A webhook form posts its answers to your own server instead of leaving them in a dashboard. In Upform you open the form's Connect panel, add a custom webhook, paste an HTTPS URL and publish. Every submitted response is then POSTed to that URL as one JSON document, carrying the question id, title, type and value for each answer. Webhooks ship on the $0 plan, next to unlimited forms and unlimited responses.
1
POST per submission
21
Question types, all typed
∞
Responses, free plan
$0
Cost of the integration
Setup
Four steps to a working form webhook
There is no SDK to install and no key to rotate on day one. The whole thing is a URL and a handler that reads JSON.
Open Connect on the form
Every form in the form builder has a right-hand panel: Question settings, Design, Logic, Connect, Share, Results. Custom webhooks sit in Connect, in the same list as Google Sheets, Slack and Notion.
Paste an HTTPS endpoint
A route in your API, a serverless function, a queue producer or anything that answers a POST. Use a long, unguessable path segment. Until you verify a signature, that URL is the only thing standing between your database and a stranger.
Submit the form once yourself
Publish it, fill it in, and log the raw request body before you parse it. One real payload teaches you more than any reference table, and you will want those exact bytes for signature checks.
Stack the rest on top
A form can write to your endpoint and to Google Sheets and to Slack at once. Destinations run independently, so a slow handler on your side does not hold up the others.
Connect · Custom webhook
- One connection per form, changed or removed without republishing the questions
- Answers keep landing in the Upform responses table regardless of what your endpoint does
- Free plan, no response cap, no per-call pricing
The payload
What actually lands on your endpoint
One document per submission. The envelope identifies the form and the submission; the answers array carries the questions in the order they appear in the builder, each one tagged with its type so you never have to guess at a string.
POST /hooks/upform HTTP/1.1
Host: api.northgate.io
Content-Type: application/json
User-Agent: Upform-Webhook/1
X-Upform-Signature: sha256=4e9c1a...{
"event": "form.submitted",
"form": {
"id": "frm_8fQ2mM4cR1",
"title": "Project brief"
},
"submission": {
"id": "sub_0KpZ7v3Nd9",
"submitted_at": "2026-09-21T14:02:37.418Z",
"completed": true,
"completion_time_seconds": 143
},
"answers": [
{
"question_id": "q_01",
"title": "What should we call you?",
"type": "full_name",
"value": "Maya Okonkwo"
},
{
"question_id": "q_02",
"title": "Work email",
"type": "email",
"value": "maya@northgate.io"
},
{
"question_id": "q_03",
"title": "Best number to reach you on",
"type": "phone",
"value": {
"country": "GB",
"dial_code": "+44",
"number": "7700 900142"
}
},
{
"question_id": "q_04",
"title": "Where should we send the contract?",
"type": "address",
"value": {
"line1": "4 Ashfield Road",
"line2": "",
"city": "Manchester",
"state": "",
"postal_code": "M14 6TR",
"country": "United Kingdom"
}
},
{
"question_id": "q_05",
"title": "Which services do you need?",
"type": "multiple_choice",
"value": ["Brand identity", "Website design"]
},
{
"question_id": "q_06",
"title": "How soon do you want to start?",
"type": "single_choice",
"value": "Within a month"
},
{
"question_id": "q_07",
"title": "Have you worked with an agency before?",
"type": "yes_no",
"value": "Yes"
},
{
"question_id": "q_08",
"title": "Budget in USD",
"type": "number",
"value": 18000
},
{
"question_id": "q_09",
"title": "Target launch date",
"type": "date",
"value": "2026-11-16"
},
{
"question_id": "q_10",
"title": "How would you rate the proposal?",
"type": "star_rating",
"value": 5
},
{
"question_id": "q_11",
"title": "Upload your current brand guidelines",
"type": "file_upload",
"value": "https://files.upform.com/frm_8fQ2mM4cR1/sub_0KpZ7v3Nd9/brand-guidelines.pdf"
},
{
"question_id": "q_12",
"title": "Anything else we should know?",
"type": "long_text",
"value": "We are merging two product lines and the naming has to hold up in both markets."
}
]
}Statement and Thank You steps collect nothing, so they never appear in the answers array. A question skipped by a conditional logic rule is absent too, so check for the key rather than for an empty string.
Typing
What each question type puts in the value field
Twenty-one question types collapse into six value shapes. Switch on the type field, never on the question title, so renaming a question in the builder cannot break your handler.
| Question types | Shape of the value | Example |
|---|---|---|
| Short Text, Long Text, Email, Full Name, Company Name, Website | String | "maya@northgate.io" |
| Number, Rating, Star Rating, Slider Scale | Number | 5 |
| Date | String, ISO 8601 date | "2026-11-16" |
| Single Choice, Dropdown, Yes/No | String, the label as the respondent saw it | "Within a month" |
| Multiple Choice, Picture Choice | Array of strings | ["Brand identity", "Website design"] |
| Phone Number | Object, with the country and dial code the flag picker set | { "dial_code": "+44", … } |
| Address | Object, one key per sub-field you left switched on | { "city": "Manchester", … } |
| File Upload | String, an HTTPS URL to the stored file | "https://files.upform.com/…" |
| Statement, Thank You | Absent (these steps take no input) | Not sent |
Uploaded files are stored by Upform and scoped to the form that collected them. See file uploads for allowed types and size limits, and for the 100 MB free-plan storage ceiling.
Security
Verifying the request
A webhook URL is a public door. Anyone who learns it can POST invented responses at your CRM, and the body alone will never tell you. A signature will.
The n8n integration is the pattern to copy. Every request Upform sends to an n8n Webhook node carries an X-Upform-Signature header holding an HMAC-SHA256 of the raw request body, keyed on a shared secret. Your side computes the same hash and compares the two. If they differ, the request did not come from Upform and nothing downstream should run.
- Hash the raw bytes, before any JSON parsing. Parse and re-serialize and the bytes change, so the hash never matches.
- Compare in constant time. A plain string equality leaks the secret one byte at a time to anyone patient.
- Reject before you act. Return 401 and log it; do not verify after you have already written the row.
- Keep treating the body as untrusted input even once it verifies. Public forms are filled in by the public.
import crypto from 'node:crypto';
const secret = process.env.UPFORM_WEBHOOK_SECRET;
// rawBody: Buffer, exactly as received
// header: the X-Upform-Signature value
export function isFromUpform(rawBody, header) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header ?? '');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Being straight with you
Retries and failures
Most webhook docs quote a retry count and a backoff curve. We would rather tell you what is true and let you design around the assumption that never breaks.
What we can tell you
- The call is dispatched when the response is submitted, not on a schedule and not on a sync you trigger
- Abandoned responses never fire. An incomplete stays in analytics and in the drop-off funnel
- Destinations are independent, so your endpoint timing out does not stop the Slack message or the sheet row
- Nothing is lost if the call fails. The response is already stored in Upform, searchable and exportable as CSV
What we will not invent
- An exact number of retry attempts
- A published backoff curve or request timeout
- A delivery log in the dashboard or a replay button
- An exactly-once guarantee (no HTTP webhook anywhere can honestly offer one)
Make your endpoint idempotent
Design for the assumption that always holds: your handler may see the same submission twice. That single habit makes retry behavior something you never have to ask about.
Key everything on the submission id
Store it with a unique constraint. A second arrival for an id you already hold is a no-op that still returns 200, not a duplicate lead and not an error.
Acknowledge fast, work later
Verify, write the raw payload somewhere durable, return 2xx, and do the enrichment on a queue. A handler that calls three APIs before responding is a handler that times out.
Never fail on someone else's outage
If your CRM is down, that is a job to retry on your side, not a reason to reject the call. Once you have the body, delivery is done.
Reconcile against the responses table
Every submission is stored in Upform with search, a date filter, CSV export and a side drawer per response. That is your source of truth when a night of logs disagrees with your database.
Choosing
When to use a webhook instead of an integration
Upform ships eight native destinations. A webhook is not a better version of them. It is the option for the places nobody built a connector for, and the one that costs you engineering time.
Reach for a webhook when
- The destination is your own product, an internal CRM or a database you own
- The answers need work before they are useful: deduping, scoring, enrichment, routing by territory
- A submission has to start something: provision an account, open a ticket, kick off a build
- You want the raw payload archived exactly as sent, for audit or replay
Use a native integration when
- The destination is Sheets, Slack, Notion, HubSpot, Mailchimp, Klaviyo or monday.com, and it is already built and mapped
- Nobody on the team wants to own a handler at 11pm on a Friday
- Non-technical colleagues need to read the answers this afternoon
- The workflow is a fan-out to many tools. That is what n8n is for, and it needs no code
Frequently asked questions
What is a webhook form?
A webhook form is a form that hands its answers to code you control instead of only storing them in a dashboard. When somebody submits it, the form builder makes an HTTP POST to a URL you chose, with the response in the body as JSON. Your endpoint does whatever it likes with it: writes a database row, scores a lead, starts a job. In Upform you add one in the form's Connect panel.
Does Upform have a forms API?
Webhooks are the outbound half of it, and they are the half most people mean when they search for a forms API: one POST per submission, typed JSON, no polling. There is no public REST API today for creating forms or reading responses back out programmatically, and Upform is not a headless form builder, so the runtime that renders the questions stays ours. If you need answers in bulk, the responses table has CSV export.
How do I verify that a webhook call came from Upform?
With an HMAC signature, the way the n8n integration works. Every request Upform sends to an n8n Webhook node carries an X-Upform-Signature header holding an HMAC-SHA256 hash of the raw request body. Your handler computes the same hash with the shared secret and compares the two in constant time. Hash the raw bytes before any JSON parsing, because re-serializing changes them and the comparison will never match.
Does Upform retry a webhook if my endpoint is down?
We will not quote you a retry count or a backoff curve, because those are the kind of numbers that change quietly and then break somebody's integration. Build for the assumption that is always safe instead: your endpoint may see the same submission more than once, so make it idempotent on the submission id. Nothing is lost either way, since every response is stored in Upform with CSV export.
Do incomplete responses trigger the webhook?
No. The call is dispatched on submit, so somebody who opens the form and abandons it at question three never reaches your endpoint. They are still counted in Upform analytics as an incomplete, and the per-question drop-off funnel names the exact question they stopped on. That keeps your database clean and keeps the diagnosis where it belongs.
Do I need a paid plan to use webhooks?
No. Custom webhooks ship on the free plan, which is $0 forever and carries unlimited forms, unlimited responses, all 21 question types and conditional logic. Pro is $12 a month and adds unlimited file storage, branding removal and up to five team members. Online checkout is not live yet, so Pro accounts are set up by hand and the team replies the same day.
Get started
Build the form. Own the payload.
A conversational form people finish, and its answers on your endpoint the second they submit. For nothing, with no response cap.
No credit card. Unlimited forms and responses on the free plan.