Skip to content

Instantly share code, notes, and snippets.

@gitfvb
Created April 11, 2025 15:46
Show Gist options
  • Save gitfvb/43840e8006deca4ede4bddf32d825261 to your computer and use it in GitHub Desktop.
Save gitfvb/43840e8006deca4ede4bddf32d825261 to your computer and use it in GitHub Desktop.
Create webhooks receiver with python and optionally IIS support and optionally mssql write support

Install python (if not already done) and modules

Download and install python: https://www.python.org/downloads/

Then open a powershell window and install flask

pip install flask

If you are interested in the install location, you can see it with

pip show flask

Then install waitress as professional web service

pip install waitress

Create a sample app

Create a folder somewhere and create a file with name app.py

from waitress import serve
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello world from Waitress behind IIS!"

if __name__ == '__main__':
    serve(app, host='127.0.0.1', port=5000, ident='MyApp', threads=8)

If you now execute the file in PowerShell with

python app.py

You should be able to open the webpage in a local browser with http://localhost:5000

IIS reverse proxy

Next step is to make it available via IIS reverse proxy. Create a folder in iis like C:\inetpub\wwwroot\webhook and use URL rewriting on that created subfolder

grafik

Just add a reverse proxy rule:

grafik

Install ARR, if not already done and do a iisreset:

grafik

grafik

Then you should be able to access the python app with your domain and subfolder name like https://www.example.com/webhook

To turn the python app off, just press Ctrl + c twice in the powershell window.

Webhook receiver

If everything was successful, we can create a new python receiver with more advanced functionality

Create a project folder somewhere and create a file like receiver.py

from waitress import serve
from flask import Flask, request, jsonify
import uuid
import os

app = Flask(__name__)

@app.route('/', methods=['POST'])
def webhook():
    if request.method == 'POST':
        data = request.json
        print(f"Received webhook data: {data}")
        process_data(request.data) # Process the webhook data here
        return jsonify({"message": "Webhook received!"}), 200
    else:
        return jsonify({"message": "Only POST requests are accepted!"}), 400
        
def process_data(data):
    # process data here
    random_file_name = f"{uuid.uuid4()}.txt"
    random_file_path = os.path.join(os.getcwd(), random_file_name)
    with open(random_file_path, "wb") as file:
        file.write(data)
    #pass

if __name__ == '__main__':
    serve(app, host='127.0.0.1', port=5000, ident='MyApp', threads=8)

Start the script with python receiver.py from PowerShell

This script creates a local file with every POST request and puts the payload in it

Now you can call it locally with PowerShell like

Invoke-WebRequest -Method POST -Uri "http://localhost:5000"  -ContentType "application/json" -Body '{"event":"order_created","order_id":12345,"customer":"John Doe"}'

or via your reverse proxy

Invoke-WebRequest -Method POST -Uri "https://www.example.com/webhook" -ContentType "application/json" -Body '{"event":"order_created","order_id":12345,"customer":"John Doe"}'

Optional: Use python receiver as windows service

For a better integration it could be useful to create a windows service. To do that you need to install pywin32

pip install pywin32

Then create a service installation script waitress_service.py:

import win32serviceutil
import win32service
import win32event
import servicemanager

from waitress import serve
from receiver import app

class WaitressService(win32serviceutil.ServiceFramework):
    _svc_name_ = "WaitressService"
    _svc_display_name_ = "Python Waitress Service"
 
    def __init__(self, args):
        win32serviceutil.ServiceFramework.__init__(self, args)
        self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
 
    def SvcStop(self):
        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
        win32event.SetEvent(self.hWaitStop)
 
    def SvcDoRun(self):
        servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
                            servicemanager.PYS_SERVICE_STARTED,
                            (self._svc_name_, ''))
        serve(app, host='127.0.0.1', port=5000, ident='MyApp', threads=8)
 
if __name__ == '__main__':
    win32serviceutil.HandleCommandLine(WaitressService)

Just for explanation: The important part is that this script is looking for receiver.py because of from receiver and imports the defined app

Install it with:

python waitress_service.py install

Then you can start your newly created windows service.

The output of this script will by default land in the directory of the service like C:\Users\Administrator\AppData\Local\Programs\Python\Python313

Then you can make changes in the script like defining an absolute path to write the payload

from waitress import serve
from flask import Flask, request, jsonify
import uuid
import os

app = Flask(__name__)

@app.route('/', methods=['POST'])
def webhook():
    if request.method == 'POST':
        data = request.json
        print(f"Received webhook data: {data}")
        process_data(request.data) # Process the webhook data here
        return jsonify({"message": "Webhook received!"}), 200
    else:
        return jsonify({"message": "Only POST requests are accepted!"}), 400
        
def process_data(data):
    # process data here
    random_file_name = f"{uuid.uuid4()}.txt"
    random_file_path = os.path.join("D:\\Scripts\\python\\flask\\webhooks", random_file_name) # use os.getcwd() as current directoy when executing it locally
    with open(random_file_path, "wb") as file:
        file.write(data)
    #pass

if __name__ == '__main__':
    serve(app, host='127.0.0.1', port=5000, ident='MyApp', threads=8)

After saving it and restart the service, the changes should be reflected immediatly. When the python script cannot be shut down, you can just kill the pythonservice.exe process.

Optional: Write into Microsoft SQL Server (mssql)

Ensure that the ODBC Driver for SQL Server is installed on your machine. You can download it from the Microsoft website if it's not already installed.

Install pyodbc with

pip install pyodbc

Create a database table in SQLServer like this

USE [Live_Webhooks]
GO

/****** Object:  Table [dbo].[Incoming]    Script Date: 10.04.2025 18:12:59 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Incoming](
	[id] [int] IDENTITY(1,1) NOT NULL,
	[service] [varchar](50) NULL,
	[webhook] [varchar](50) NULL,
	[object] [varchar](50) NULL,
	[object_id] [varchar](50) NULL,
	[event] [varchar](50) NULL,
	[payload] [varchar](max) NULL,
	[created_at] [datetime] NOT NULL,
 CONSTRAINT [PK_Incoming] PRIMARY KEY CLUSTERED 
(
	[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

ALTER TABLE [dbo].[Incoming] ADD  CONSTRAINT [DF_Incoming_created_at]  DEFAULT (getdate()) FOR [created_at]
GO

And a user

USE [master]
GO

/* For security reasons the login is created disabled and with a random password. */
CREATE LOGIN [webhooks_service] WITH PASSWORD=N'xT6lWzmqxDPFmVGNVlOKbhHK61h9q+ff3u0W7AH0Jzg=', DEFAULT_DATABASE=[Live_Webhooks], DEFAULT_LANGUAGE=[us_english], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF
GO

ALTER LOGIN [webhooks_service] DISABLE
GO

USE [Live_Webhooks]
GO
CREATE USER [webhooks_service] FOR LOGIN [webhooks_service] WITH DEFAULT_SCHEMA=[dbo]
GO

That user needs to be activated first.

Then change the script to something like this

[ ] create the rest

Use a scalable solution with fastapi and async functions

This is mostly created by DeepSeek

Install dependencies

pip install sqlalchemy, typing, uvicorn, fastapi, aioodbc

Create python script

import os
import logging
from datetime import datetime
from fastapi import FastAPI, Request, HTTPException, Depends, Header
from fastapi.responses import JSONResponse
from contextlib import asynccontextmanager
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import Column, Integer, String, Text, DateTime, MetaData
from sqlalchemy.ext.declarative import declarative_base
import uvicorn
import urllib.parse
from typing import Optional


# ----------------------------
# CONFIGURATION
# ----------------------------

# TODO Change current directoy for relative files
os.chdir("D:/Scripts/python/flask/webhooks")

STATIC_TOKEN = os.getenv('STATIC_TOKEN', 'your-static-secret-token-here')
 
# TODO MSSQL Konfiguration
server = os.getenv('MSSQL_SERVER', 'localhost')
database = os.getenv('MSSQL_DATABASE', 'Live_Webhooks')
username = os.getenv('MSSQL_USERNAME', 'webhooks_service')
password = os.getenv('MSSQL_PASSWORD', 'verysecretpassword')
driver = os.getenv('MSSQL_DRIVER', 'ODBC Driver 17 for SQL Server')
 
password_encoded = urllib.parse.quote_plus(password)
DATABASE_URL = f"mssql+aioodbc://{username}:{password_encoded}@{server}/{database}?driver={driver}"


# ----------------------------
# CONFIGURE LOGGING
# ----------------------------

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('fastapi_mssql.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)


# ----------------------------
# DEFINE LIFESPAN EVENTS
# ----------------------------

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Everything until yield gets executed at the start, everything after that at the end
    await reflect_tables()
    logger.info("Datenbankmetadaten wurden geladen")
    logger.info(f"Verfügbare Tabellen: {list(metadata.tables.keys())}")
    yield
    logger.info("Closing database connection")
    sess = get_db()
    sess.close()


# ----------------------------
# FASTAPI APP
# ----------------------------

app = FastAPI(
    title="Apteco Webhooks Live Event Receiver",
    description="Receive events and store them into SQLServer",
    version="0.0.1",
    lifespan=lifespan
)


# ----------------------------
# CONNECT TO DATABASE AND READ METADATA MODEL
# ----------------------------

engine = create_async_engine(
    DATABASE_URL,
    pool_size=5,
    max_overflow=10,
    pool_recycle=300,
    pool_pre_ping=True
)
 
# Metadaten aus bestehender Datenbank laden
metadata = MetaData()

async def reflect_tables():
    async with engine.begin() as conn:
        await conn.run_sync(metadata.reflect)

# Automatische Tabellenreflektion beim Start
# @app.on_event("startup")
# async def startup():
#     await reflect_tables()
#     logger.info("Datenbankmetadaten wurden geladen")
#     logger.info(f"Verfügbare Tabellen: {list(metadata.tables.keys())}")

AsyncSessionLocal = sessionmaker(
    bind=engine,
    class_=AsyncSession,
    expire_on_commit=False
)


# ----------------------------
# MORE DEPENDENCIES AND FUNCTIONS
# ----------------------------

async def get_db():
    async with AsyncSessionLocal() as session:
        yield session

# TODO add another function to check a token in the url
async def verify_token(authorization: str = Header(...)):
    if not authorization.startswith("Bearer "):
        logger.warning("Ungültiges Authorization Header Format")
        raise HTTPException(status_code=401, detail="Ungültiges Token Format")
 
    token = authorization.split(" ")[1]
    if token != STATIC_TOKEN:
        logger.warning(f"Ungültiger Token verwendet: {token}")
        raise HTTPException(status_code=401, detail="Ungültiger Token")
 
    return token


# ----------------------------
# ROUTES
# ----------------------------

# TODO CHECK: This is an endpoint
@app.post("/payload")
async def create_payload(
    request: Request,
    db: AsyncSession = Depends(get_db),
    token: str = Depends(verify_token) # TODO CHECK: This is checking the token, could also be a different function
):
    try:
        data = await request.json()
        logger.info(f"Neue Anfrage mit Daten: {str(data)[:200]}...")
 
        if not data:
            logger.warning("Leere Anfrage erhalten")
            return JSONResponse(
                content={"status": "error", "message": "Keine Daten empfangen"},
                status_code=400
            )
 
        # Zugriff auf die bestehende Tabelle
        payloads_table = metadata.tables['Incoming']
 
        # Insert-Operation
        # TODO you need to reflect the data that needs to be filled
        insert_stmt = payloads_table.insert().values(
            service='test',
            object='testobject',
            object_id=data['order_id'],
            event='new',
            webhook=str(request.url.path),
            payload=str(data)
            #timestamp=datetime.utcnow()
        )
 
        await db.execute(insert_stmt)
        await db.commit()
 
        logger.info("Daten erfolgreich in bestehende Tabelle geschrieben")
 
        return JSONResponse({
            "status": "success",
            "message": "Daten in MSSQL gespeichert"
        })
    except KeyError:
        logger.error("Tabelle 'payloads' nicht gefunden")
        return JSONResponse(
            content={"status": "error", "message": "Zieltabelle nicht gefunden"},
            status_code=500
        )
    except Exception as e:
        await db.rollback()
        logger.error(f"Fehler bei Anfrageverarbeitung: {str(e)}", exc_info=True)
        return JSONResponse(
            content={"status": "error", "message": str(e)},
            status_code=500
        )
 

# ----------------------------
# MAIN
# ----------------------------

if __name__ == "__main__":
    logger.info("FastAPI Server wird gestartet...")
    uvicorn.run(
        app,
        host="0.0.0.0",
        port=5000,
        log_config=None
    )

This script could also be started as a windows service like in the examples above.

  • implement a global token and one per entry?

Execute it

python receiver_fast.py

Test it

To test it from the same server, you could execute a call via PowerShell like

invoke-webrequest -UseBasicParsing -Uri "http://localhost:5000/payload" -ContentType "application/json" -Method POST -Body '{"event":"order_created","order_id":12345,"customer":"John Doe"}'

which should trigger an error because the token is missing.

This one should be working then

invoke-webrequest -UseBasicParsing -Uri "http://localhost:5000/payload" -ContentType "application/json; charset=utf-8 " -Method POST -Body '{"event":"order_created","order_id":123456,"customer":"John Döe"}' -Headers @{"Authorization"="Bearer your-static-secret-token-here"}

or like this from external (don't forget the https:):

invoke-restmethod -Uri "https://www.example.com/webhook/payload" -ContentType "application/json; charset=utf-8 " -Method POST -Body '{"event":"order_created","order_id":123,"customer":"John De"}' -Headers @{"Authorization"="Bearer your-static-secret-token-here"}

Tips from DeepSeek

  • For production environments, use process managers like winsw verwenden
  • Activate logging for debugging purposes serve(app, host='127.0.0.1', port=5000, ident='MyApp')
  • Increase the threads for waitress: serve(app, host='127.0.0.1', port=5000, threads=8)
  • Ensure that the firewall allows traffic for port 5000 and localhost
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment