Skip to navigation

Webhooks API Walkthroughs

Real-time notifications for form responses using webhooks

Webhooks allow you to receive real-time HTTP notifications when someone submits a response to your typeform. Instead of polling the Responses API, webhooks push data to your server immediately upon form submission.

How webhooks work

When a respondent submits your typeform:

  1. Typeform sends an HTTP POST request to your webhook URL
  2. The request contains the complete response data
  3. Your server processes the data and responds with a 200 status code
  4. If delivery fails, Typeform retries automatically

Webhooks require a publicly accessible HTTPS endpoint. Your server must respond within 10 seconds to be considered successful.

Create a webhook

Use the Webhooks API to create a webhook for your form:

PUT https://api.typeform.com/forms/{form_id}/webhooks/{tag}

The tag is a unique identifier for your webhook (e.g., production, staging, crm-sync).

Request body:

{
"url": "https://your-server.com/webhooks/typeform",
"enabled": true
}

Example:

curl -X PUT \
'https://api.typeform.com/forms/abc123/webhooks/production' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://your-server.com/webhooks/typeform",
"enabled": true
}'

Response:

{
"id": "webhook_123xyz",
"form_id": "abc123",
"tag": "production",
"url": "https://your-server.com/webhooks/typeform",
"enabled": true,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z"
}

List webhooks

Get all webhooks for a form:

GET https://api.typeform.com/forms/{form_id}/webhooks

Response:

{
"items": [
{
"id": "webhook_123xyz",
"form_id": "abc123",
"tag": "production",
"url": "https://your-server.com/webhooks/typeform",
"enabled": true,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:00Z"
}
]
}

Get a specific webhook

GET https://api.typeform.com/forms/{form_id}/webhooks/{tag}

Update a webhook

Use the same PUT endpoint to update an existing webhook:

PUT https://api.typeform.com/forms/{form_id}/webhooks/{tag}

Request body:

{
"url": "https://your-new-server.com/webhooks/typeform",
"enabled": true
}

Delete a webhook

DELETE https://api.typeform.com/forms/{form_id}/webhooks/{tag}

Successful deletion returns a 204 No Content status.

Webhook payload structure

When a form is submitted, Typeform sends a POST request to your webhook URL with this structure:

{
"event_id": "01HKMN8VCFX1ABC123XYZ",
"event_type": "form_response",
"form_response": {
"form_id": "abc123",
"token": "response_token_xyz",
"landed_at": "2024-01-15T10:30:00Z",
"submitted_at": "2024-01-15T10:35:25Z",
"calculated": {
"score": 85
},
"hidden": {
"user_id": "12345",
"source": "email"
},
"definition": {
"id": "abc123",
"title": "Customer Satisfaction Survey",
"fields": [
{
"id": "field1",
"title": "How satisfied are you?",
"type": "opinion_scale",
"ref": "satisfaction",
"properties": {}
}
]
},
"answers": [
{
"type": "number",
"number": 9,
"field": {
"id": "field1",
"type": "opinion_scale",
"ref": "satisfaction"
}
}
]
}
}

Key components

  • event_id: Unique identifier for this webhook event
  • event_type: Always "form_response" for submissions
  • form_response: Contains the complete response data
    • form_id: Your form’s ID
    • token: Unique response token
    • landed_at/submitted_at: Timestamps
    • calculated: Scores and calculated values
    • hidden: Hidden field values
    • definition: Form structure (questions)
    • answers: Response data

The webhook payload includes the form definition, so you have context about which questions the answers correspond to.

Implementing a webhook endpoint

Node.js/Express example

const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/webhooks/typeform', async (req, res) => {
try {
// 1. Verify the webhook (see security section)
if (!verifyWebhookSignature(req)) {
return res.status(401).send('Invalid signature');
}
// 2. Extract the response data
const { event_id, event_type, form_response } = req.body;
if (event_type !== 'form_response') {
return res.status(200).send('OK'); // Acknowledge but ignore
}
// 3. Process the response
await processResponse(form_response);
// 4. Respond quickly (within 10 seconds)
res.status(200).send('OK');
// 5. Do heavy processing asynchronously
setImmediate(async () => {
await sendToAnalytics(form_response);
await updateCRM(form_response);
await sendNotificationEmail(form_response);
});
} catch (error) {
console.error('Webhook error:', error);
res.status(500).send('Internal server error');
}
});
async function processResponse(formResponse) {
// Extract answer values
const answers = {};
formResponse.answers.forEach((answer) => {
const ref = answer.field.ref;
switch (answer.type) {
case 'text':
case 'email':
case 'url':
answers[ref] = answer[answer.type];
break;
case 'number':
answers[ref] = answer.number;
break;
case 'choice':
answers[ref] = answer.choice.label;
break;
case 'choices':
answers[ref] = answer.choices.labels;
break;
case 'boolean':
answers[ref] = answer.boolean;
break;
}
});
// Save to database
await db.responses.create({
formId: formResponse.form_id,
responseToken: formResponse.token,
submittedAt: new Date(formResponse.submitted_at),
answers: answers,
hiddenFields: formResponse.hidden,
score: formResponse.calculated?.score,
});
}
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});

Python/Flask example

from flask import Flask, request, jsonify
import hmac
import hashlib
import json
app = Flask(__name__)
@app.route('/webhooks/typeform', methods=['POST'])
def typeform_webhook():
try:
# 1. Get the webhook data
data = request.get_json()
# 2. Verify signature (see security section)
if not verify_signature(request):
return jsonify({'error': 'Invalid signature'}), 401
# 3. Process the response
if data['event_type'] == 'form_response':
process_response(data['form_response'])
# 4. Return success quickly
return jsonify({'status': 'success'}), 200
except Exception as e:
print(f'Error processing webhook: {e}')
return jsonify({'error': 'Internal error'}), 500
def process_response(form_response):
# Extract answers
answers = {}
for answer in form_response['answers']:
ref = answer['field']['ref']
answer_type = answer['type']
if answer_type in ['text', 'email', 'url']:
answers[ref] = answer[answer_type]
elif answer_type == 'number':
answers[ref] = answer['number']
elif answer_type == 'choice':
answers[ref] = answer['choice']['label']
elif answer_type == 'choices':
answers[ref] = answer['choices']['labels']
elif answer_type == 'boolean':
answers[ref] = answer['boolean']
# Save to database
save_to_database(form_response, answers)
if __name__ == '__main__':
app.run(port=3000)

Common use cases

1. Instant email notifications

Send email alerts immediately when forms are submitted:

async function sendNotificationEmail(formResponse) {
const answers = extractAnswers(formResponse);
await emailService.send({
to: 'team@company.com',
subject: `New form submission: ${formResponse.form_id}`,
html: formatAnswersAsHTML(answers),
});
}

2. CRM integration

Automatically create or update CRM records:

async function updateCRM(formResponse) {
const answers = extractAnswers(formResponse);
await crmAPI.createContact({
email: answers.email,
name: answers.name,
company: answers.company,
source: 'Typeform',
formId: formResponse.form_id,
});
}

3. Slack notifications

Post to Slack when important forms are submitted:

async function sendToSlack(formResponse) {
const answers = extractAnswers(formResponse);
await slackAPI.postMessage({
channel: '#customer-feedback',
text: `New feedback received!`,
attachments: [
{
fields: Object.entries(answers).map(([key, value]) => ({
title: key,
value: value,
short: true,
})),
},
],
});
}

4. Analytics tracking

Send events to analytics platforms:

async function sendToAnalytics(formResponse) {
await analyticsAPI.track({
event: 'Form Submitted',
properties: {
formId: formResponse.form_id,
formTitle: formResponse.definition.title,
score: formResponse.calculated?.score,
timestamp: formResponse.submitted_at,
},
});
}

Testing webhooks locally

Use tools like ngrok to expose your local server:

# Install ngrok
npm install -g ngrok
# Start your local server
node server.js
# In another terminal, start ngrok
ngrok http 3000
# Use the ngrok URL (https://abc123.ngrok.io) as your webhook URL

Retry mechanism

If your endpoint fails to respond with a 2xx status code, Typeform automatically retries:

  • Immediate retry
  • Retry after 1 minute
  • Retry after 5 minutes
  • Retry after 30 minutes
  • Retry after 2 hours

After 5 failed attempts, the webhook is disabled.

Make sure your endpoint is idempotent. You may receive the same webhook multiple times due to retries, so use the event_id to deduplicate.

Handling duplicates

const processedEvents = new Set();
app.post('/webhooks/typeform', async (req, res) => {
const { event_id } = req.body;
// Check if we've already processed this event
if (processedEvents.has(event_id)) {
console.log(`Duplicate event ${event_id}, ignoring`);
return res.status(200).send('OK');
}
// Process the event
await processResponse(req.body.form_response);
// Mark as processed
processedEvents.add(event_id);
res.status(200).send('OK');
});

For production, use a database or cache:

async function isDuplicate(eventId) {
const exists = await redis.get(`webhook:${eventId}`);
if (exists) return true;
await redis.setex(`webhook:${eventId}`, 86400, '1'); // 24 hour TTL
return false;
}

Best practices

  1. Respond quickly: Return 200 status within 10 seconds
  2. Process asynchronously: Do heavy work after responding
  3. Handle duplicates: Use event_id to prevent duplicate processing
  4. Verify signatures: Always verify webhook authenticity (see security guide)
  5. Log everything: Keep detailed logs of webhook deliveries
  6. Monitor failures: Track and alert on webhook failures
  7. Use HTTPS: Webhooks require HTTPS endpoints
  8. Test thoroughly: Test with actual form submissions before going live

Monitoring webhooks

Track webhook health:

const webhookMetrics = {
received: 0,
processed: 0,
failed: 0,
duplicates: 0,
};
app.post('/webhooks/typeform', async (req, res) => {
webhookMetrics.received++;
try {
if (await isDuplicate(req.body.event_id)) {
webhookMetrics.duplicates++;
return res.status(200).send('OK');
}
await processResponse(req.body.form_response);
webhookMetrics.processed++;
res.status(200).send('OK');
} catch (error) {
webhookMetrics.failed++;
console.error('Webhook processing failed:', error);
res.status(500).send('Error');
}
});
// Metrics endpoint
app.get('/metrics', (req, res) => {
res.json(webhookMetrics);
});

Troubleshooting

Webhook not receiving data

  1. Check that the webhook URL is publicly accessible
  2. Verify the URL uses HTTPS
  3. Check firewall and security group settings
  4. Test the endpoint with curl

Webhook disabled

If Typeform disables your webhook after failures:

  1. Fix the endpoint issues
  2. Re-enable via API:
PUT https://api.typeform.com/forms/{form_id}/webhooks/{tag}
{
"url": "https://your-server.com/webhooks/typeform",
"enabled": true
}

Response timeout

If your endpoint times out:

  1. Respond with 200 immediately
  2. Queue processing for background workers
  3. Optimize database queries
  4. Use caching where possible

Next steps