Skip to content
trending

Guides

What Is a Webhook? Connect AI to Forms, Email & Spreadsheets

A webhook is a URL where one app automatically sends data to another when something happens. We show you how to build one with FastAPI, AI classification, and spreadsheet logging.

By 10 min read
What Is a Webhook? Connect AI to Forms, Email & Spreadsheets

👁 116 reads

A webhook is a URL where one application automatically sends data to another the moment something happens: someone fills out a form, an email arrives, someone makes a payment. Instead of asking “is there anything new?” every minute, you wait for someone to knock. Below, we show you a webhook in practice: a local Python server (FastAPI) receives a submission from a form, an AI model classifies it and drafts a response, and the result goes into a spreadsheet. Then we describe the same thing in Make and Zapier. This article is part of the AI Agents from Scratch learning path, automation stage.

Status as of October 4, 2026

  • Test: Windows 11, Python 3.13, FastAPI 0.142.2, uvicorn 0.54.0, model openai/gpt-5.6-luna via OpenRouter.
  • One submission: 1.7-2.1 s from send to response, cost approximately 0.00016 USD.
  • Make: free plan is 1,000 credits per month and 2 active scenarios. Zapier: 100 tasks per month, only two-step Zaps, and webhooks are on paid plans (from 19.99 USD per month with annual billing).
  • Code: ai-agents-starter package, files 06_webhook.py and 06_test_webhook.py.

What Is a Webhook in Simple Terms

There are two ways for a program to find out about something new:

  • polling (asking periodically): Your program checks the inbox or spreadsheet every 5 minutes to see if something arrived. Simple, but delayed and wastes requests;
  • webhook: you give another application your address (e.g. https://yourdomain.com/webhook/form), and it sends an HTTP POST request with data in JSON there immediately when something happens.

A webhook is just a regular API request, only in reverse: you don’t call the service, the service calls you. If HTTP requests and JSON are new to you, start with the API guide.

Typical webhook sources:

  • forms (Tally, Typeform, Google Forms via script, WordPress forms);
  • payments (Stripe, PayPal notify about payment);
  • stores (WooCommerce, Shopify: new order);
  • work tools (GitHub, Slack, Trello, Calendly);
  • automation platforms (Make, Zapier and n8n have ready-made “receive webhook” modules).

What We Built

  1. A contact form sends JSON: name, email, message.
  2. Our server checks a secret in the header (so strangers can’t send junk).
  3. An AI model receives the message and returns JSON: category, urgency from 1 to 3, summary, draft response.
  4. The server appends a row to a CSV file (local equivalent of Google Sheets) and sends back the result.

Webhook Server Code

Installation: pip install fastapi uvicorn openai python-dotenv. In the .env file, add keys OPENROUTER_API_KEY and WEBHOOK_SECRET (a long random string).

"""06 - Webhook: form sends submission -> server queries model -> result goes to spreadsheet (CSV) and returns in response.
Start:  uvicorn 06_webhook:app --port 8000      (or: python 06_webhook.py)
Test:   curl -X POST http://127.0.0.1:8000/webhook/form -H "X-Webhook-Secret: ..." -H "Content-Type: application/json"
             -d "{"name":"Alice","email":"alice@example.com","message":"Invoice has wrong tax ID"}"
"""
import csv, datetime, json, os, pathlib
from dotenv import load_dotenv
from fastapi import FastAPI, Header, HTTPException
from openai import OpenAI
from pydantic import BaseModel

load_dotenv()
SECRET = os.getenv("WEBHOOK_SECRET", "change-me")       # same string you enter in Make/Zapier/form
MODEL = os.getenv("MODEL", "openai/gpt-5.6-luna")
SHEET = pathlib.Path("submissions.csv")
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"])
app = FastAPI()

class Submission(BaseModel):          # this is what the JSON from the form looks like
    name: str
    email: str
    message: str

@app.post("/webhook/form")
def receive(s: Submission, x_webhook_secret: str = Header(default="")):
    if x_webhook_secret != SECRET:     # without this, anyone who knows the address can send you junk and burn tokens
        raise HTTPException(status_code=401, detail="wrong secret")
    r = client.chat.completions.create(model=MODEL, response_format={"type": "json_object"}, messages=[
        {"role": "system", "content": "You are a customer service assistant. Return JSON with fields: category (billing|complaint|"
         "quote|other), urgency (1-3, 3 = urgent), summary (max 12 words), draft_response (2-3 sentences)."},
        {"role": "user", "content": f"From: {s.name}nMessage: {s.message}"}])
    result = json.loads(r.choices[0].message.content)
    new = not SHEET.exists()
    with SHEET.open("a", newline="", encoding="utf-8-sig") as f:   # utf-8-sig: Excel will display characters correctly
        w = csv.writer(f, delimiter=";")
        if new: w.writerow(["time", "name", "email", "category", "urgency", "summary", "draft_response"])
        w.writerow([datetime.datetime.now().isoformat(timespec="seconds"), s.name, s.email, result.get("category"),
                    result.get("urgency"), result.get("summary"), result.get("draft_response")])
    return {"ok": True, **result, "cost_usd": getattr(r.usage, "cost", None)}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)

Key points:

  • class Submission(BaseModel) describes what the JSON should look like. FastAPI will automatically reject requests without required fields (error 422), before you pay for the model;
  • secret in the X-Webhook-Secret header. A guessed address without the secret gets 401 and costs nothing;
  • response_format={"type": "json_object"} forces the model to return clean JSON, which we immediately insert into columns;
  • utf-8-sig when writing CSV makes Excel display characters correctly without workarounds.

Running and Testing

In one terminal window, we start the server: uvicorn 06_webhook:app --port 8000. In another, we run a script that pretends to be a form and sends three submissions plus one with a wrong secret:

"""Sends 3 sample submissions to the local webhook (a form or Make/Zapier would do the same)."""
import os, time, requests
URL = "http://127.0.0.1:8000/webhook/form"
SECRET = os.getenv("WEBHOOK_SECRET", "change-me")
print("wrong secret ->", requests.post(URL, json={"name": "X", "email": "x@example.com", "message": "test"},
                                     headers={"X-Webhook-Secret": "guessing"}).status_code)
for name, content in [("Alice", "Hello, invoice FV/2026/09/027 has the wrong tax ID for my company. I need a correction by Friday because I'm closing the month."),
                    ("Mark", "How much does it cost to implement a chatbot on a store website with 300 products? I'm interested in a November timeline."),
                    ("Kate", "The cable I ordered arrived damaged, the plug is cracked. I want a refund.")]:
    t = time.time()
    r = requests.post(URL, json={"name": name, "email": f"{name.lower()}@example.com", "message": content},
                      headers={"X-Webhook-Secret": SECRET}, timeout=60)
    print(f"n{name}: {content}n-> HTTP {r.status_code} in {time.time() - t:.1f} s: {r.json()}")

Result from October 4, 14:11 (we tested it in Polish):

> python 06_test_webhook.py
wrong secret -> 401

Alice: Hello, invoice FV/2026/09/027 has the wrong tax ID for my company. I need a correction by Friday because I'm closing the month.
-> HTTP 200 in 2.1 s: {'ok': True, 'category': 'billing', 'urgency': 3, 'summary': 'Wrong tax ID on invoice, correction needed by Friday', 'draft_response': 'Thanks for your message - we will check invoice FV/2026/09/027 and prepare a correction with the correct tax ID. We will handle this urgently so the document is ready by Friday.', 'cost_usd': 0.0001746}

Mark: How much does it cost to implement a chatbot on a store website with 300 products? I'm interested in a November timeline.
-> HTTP 200 in 1.8 s: {'ok': True, 'category': 'quote', 'urgency': 2, 'summary': 'Inquiry about cost and timeline for chatbot implementation', 'draft_response': 'Hi Mark, we will prepare a quote for implementing a chatbot for a store with 300 products and check availability for November. Please send the store address and describe what functions the chatbot should handle.', 'cost_usd': 0.000162}

Kate: The cable I ordered arrived damaged, the plug is cracked. I want a refund.
-> HTTP 200 in 1.7 s: {'ok': True, 'category': 'complaint', 'urgency': 2, 'summary': 'Damaged cable, cracked plug; customer wants refund.', 'draft_response': 'We are sorry the cable arrived damaged. Please send photos of the damage and the order number, and we will process your refund.', 'cost_usd': 0.0001504}

> type submissions.csv
time;name;email;category;urgency;summary;draft_response
2026-10-04T14:11:16;Alice;alice@example.com;billing;3;Wrong tax ID on invoice, correction needed by Friday;Thanks for your message - we will check invoice FV/2026/09/027 and prepare a correction with the correct tax ID. We will handle this urgently so the document is ready by Friday.
2026-10-04T14:11:18;Mark;mark@example.com;quote;2;Inquiry about cost and timeline for chatbot implementation;Hi Mark, we will prepare a quote for implementing a chatbot for a store with 300 products and check availability for November. Please send the store address and describe what functions the chatbot should handle.
2026-10-04T14:11:19;Kate;kate@example.com;complaint;2;Damaged cable, cracked plug; customer wants refund.;We are sorry the cable arrived damaged. Please send photos of the damage and the order number, and we will process your refund.

What we see:

  • request with wrong secret rejected with code 401, without calling the model;
  • categories correct in all three cases. Urgency 3 only for the issue with a deadline (“by Friday”), the rest got 2;
  • draft responses are correct, and in the complaint the model asked for photos and order number on its own;
  • the model once inserted an em dash instead of a regular hyphen. If you have style rules, add them to the system prompt.

A trap we hit: curl in Git Bash on Windows with non-ASCII characters typed in -d '...' sent broken encoding and the server responded “There was an error parsing the body” (400). The fix was to save the JSON to a file and send it via --data-binary @file.json or a Python script as above.

How to Expose a Webhook to the Internet

A server on 127.0.0.1 is only visible to your computer. For a form from the internet to knock on it, you need a public address:

  • for testing: a tunnel like ngrok or Cloudflare Tunnel, which gives you a temporary HTTPS address pointing to your port 8000;
  • permanently: a small server or application platform, e.g. Railway, where you upload the same file and get an address;
  • serverless: receive the webhook in Make, Zapier or n8n (described below) and call the model from there.

Always use HTTPS and a secret. Serious services (Stripe, GitHub) additionally sign each request with an HMAC key. Then instead of comparing the secret, you calculate a signature of the content and compare it with the header.

The Same Thing in Make (No Code)

  1. Create a scenario and add Webhooks, Custom webhook as the first module. Make will generate a URL.
  2. Paste the address in your form settings (Tally, Typeform, WordPress) and send one test submission. Make will automatically recognize the JSON structure.
  3. Add an AI module: OpenAI (or HTTP module to OpenRouter) with a prompt like in our code and JSON response.
  4. Add JSON, Parse JSON to break the response into fields.
  5. Add Google Sheets, Add a row and map fields to columns.
  6. Optionally: filter “urgency = 3” and a module sending a notification.

Each submission will use several credits in Make (one per module). The free plan gives 1,000 credits per month and 2 active scenarios. More about Make in the article Make: how it works and what you pay for.

The Same Thing in Zapier

  1. Trigger: Webhooks by Zapier, Catch Hook (address to paste in the form). Alternatively, a ready-made form trigger if Zapier supports it.
  2. Action: ChatGPT or AI by Zapier with classification prompt.
  3. Action: Google Sheets, Create Spreadsheet Row.

Webhooks by Zapier is a premium app, so you need a paid plan (Professional from 19.99 USD per month with annual billing, 29.99 USD monthly), and the entire Zap has 3 steps, which is more than the free plan allows. A comparison of Zapier, Make and n8n is in the article Zapier: what it is and how much it costs.

Code, Make or Zapier: What to Choose

Method Fixed cost Requires For whom
Own server (FastAPI) hosting, from 0 USD on your own computer with tunnel basic Python you want to understand what’s happening and not pay per step
n8n (own server) free Community edition, you pay for server setting up n8n, e.g. on VPS lots of automation, data on your side
Make from 0 USD (1,000 credits) only browser medium complexity scenarios, branching
Zapier webhooks from Professional plan only browser most ready integrations, simplest start

Frequently Asked Questions

What is a webhook in simple words?

An internet address where another application automatically sends information that something happened, along with the data of that event.

How is a webhook different from an API?

An API is a door you knock on when you need something. A webhook is a doorbell that rings at your place when something happens on the other side. Technically, a webhook is a request to your API.

Is a webhook secure?

As secure as you make it. Minimum is HTTPS and a secret in the header. Better: HMAC signature, timestamp checking (protection against replay) and data size limits.

How do I test a webhook without a public server?

Send a request from your own script to 127.0.0.1 (as in this article) or use a tunnel that redirects a public address to your computer.

What happens if my server is down?

Depends on the sender. Stripe or GitHub retry for some time. A simple form might lose the submission. That’s why a webhook server should respond quickly, and do heavier work (e.g. calling a model) in the background when there’s high traffic.

Can I connect a webhook to an AI agent?

Yes, that’s a typical way to trigger agents: an event arrives via webhook, and instead of one model query, an agent loop with tools starts. How to build one is shown in the article How to build an AI agent in Python.

Google · Your sourcesQuantslant higher in your Google - one clickAdd to preferred sources →

Sources and Verification Date

Kinetyra

// read next

More on this from QUANTSLANT

Piotr Olszewski

Editor and publisher · Quantslant

I edit and publish Quantslant. AI tools help with research, drafts and translation; choosing topics, running the tests, checking facts and the decision to publish are mine, and so is the responsibility for what appears here.

read nextAI Agent Examples: 12 Agents You Can Build This WeekendguidesAI Agent Examples: 12 Agents You Can Build This Weekend12 practical AI agent examples with difficulty levels and tools. Four tested and working: invoice processing, form triage, document chat, and MCP server.

Most read

  1. 1Best AI Video Generator Apps for Your Phone (2026)
  2. 2Claude Opus 5.5 Motion Graphics: 60s Showreel from One Prompt
  3. 3Wan 2.2 on RTX 4090: timings, VRAM and four test clips
// site map

This page in other languages6

Quantslant in numbers 1,423posts in archive22news from the last 7 days18video models in the observatory
🎬 AI Video Benchmarks: Higgsfield DoP vs OpenAI Sora • Higgsfield vs Runway Gen-3 • AI Video Credit Calculator
× ‹ enlarge ›