DEV Community

Cover image for Solved: Sync Typeform Responses to a Private Discord Admin Channel
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Sync Typeform Responses to a Private Discord Admin Channel

🚀 Executive Summary

TL;DR: This guide details how to automate Typeform response delivery to a private Discord admin channel in real-time. It leverages Typeform webhooks and a Python Flask application to parse submissions and send formatted messages via a Discord bot, eliminating manual checks and expensive third-party integrations.

🎯 Key Takeaways

  • Real-time Typeform data is pushed via webhooks to a custom endpoint configured in the Typeform ‘Connect’ panel.
  • A Python Flask application acts as the webhook listener, processing incoming JSON payloads from Typeform submissions.
  • Discord bot integration, requiring a DISCORD\_BOT\_TOKEN and DISCORD\_CHANNEL\_ID, is used to post formatted messages (embeds) to a private channel via the Discord API.
  • python-dotenv is crucial for securely managing sensitive credentials like Discord bot tokens and channel IDs as environment variables.
  • ngrok provides a temporary publicly accessible URL for local Flask development, enabling Typeform to send webhooks during testing phases.
  • The format\_typeform\_response\_for\_discord function dynamically structures Typeform answers and form definitions into human-readable Discord embeds for better presentation.

Sync Typeform Responses to a Private Discord Admin Channel

In the fast-paced world of DevOps, SysAdmins, and Developers, timely information is paramount. Waiting for daily reports or manually checking platforms for new data can lead to missed opportunities, delayed responses, or an overall sluggish workflow. This is especially true when critical user feedback, support requests, or lead generations flow in through tools like Typeform.

Imagine your team needs instant notifications when a new Typeform submission comes in for an application, a bug report, or a high-priority contact request. Manually refreshing the Typeform dashboard is tedious and inefficient. Relying on email notifications can clutter inboxes and delay the visibility for an entire team. Many commercial SaaS solutions offer integrations, but they often come with a price tag and might not offer the granular control or customization you need.

This tutorial from TechResolve will guide you through building a robust, cost-effective solution to sync Typeform responses directly to a private Discord admin channel in real-time. By leveraging webhooks and a simple Python Flask application, you’ll ensure your team is always in the loop, enabling quicker decision-making and more efficient response times. Say goodbye to manual checks and expensive third-party connectors, and hello to instant, actionable insights.

Prerequisites

Before we dive into the integration, ensure you have the following components and knowledge:

  • Typeform Account and Form: An active Typeform account with at least one form created that you wish to monitor.
  • Discord Server and Admin Channel: A Discord server where your team operates and a specific private text channel designated for administrative notifications.
  • Python 3.x: Installed on your local machine or server. We’ll be using Python for our webhook listener.
  • pip: Python’s package installer, usually included with Python 3.x.
  • Flask: A lightweight Python web framework. Install it using pip install Flask python-dotenv requests.
  • python-dotenv: For managing environment variables securely.
  • requests: Python HTTP library for making API calls.
  • Publicly Accessible Endpoint: For development, tools like ngrok are excellent for exposing your local development server to the internet. For production, you’ll need a dedicated server or a serverless function (e.g., AWS Lambda, Google Cloud Functions).

Step-by-Step Guide

1. Set Up Your Discord Bot and Admin Channel

To enable your Discord server to receive automated messages, you’ll need to create a bot user and grant it the necessary permissions.

  1. Create a Discord Application and Bot User:
  • Go to the Discord Developer Portal.
  • Click “New Application”. Give it a name (e.g., “Typeform Sync Bot”).
  • Once created, navigate to the “Bot” tab on the left sidebar.
  • Click “Add Bot”, then “Yes, do it!” to confirm.
  • Under “Privileged Gateway Intents”, enable “MESSAGE CONTENT INTENT”. This is crucial for the bot to read message content if you ever want to expand its functionality, though for sending messages, it’s not strictly required, it’s a good practice.
  • Copy Your Bot Token: Under the “Token” section, click “Reset Token” (if you haven’t yet) and then “Copy”. Store this securely; it’s like a password for your bot. We will refer to this as DISCORD_BOT_TOKEN.

    1. Add Bot to Your Server and Get Channel ID:
  • In the Developer Portal, go to “OAuth2” > “URL Generator”.

  • Under “SCOPES”, select bot.

  • Under “BOT PERMISSIONS”, select Send Messages. This is the minimum required permission.

  • A URL will be generated at the bottom. Copy it and paste it into your browser. Select your server from the dropdown and authorize the bot.

  • Once the bot is in your server, go to your designated private admin channel.

  • To get the channel ID, enable Developer Mode in your Discord client (User Settings > Advanced > Developer Mode).

  • Right-click the admin channel in Discord and select “Copy ID”. We will refer to this as DISCORD_CHANNEL_ID.

2. Configure Your Typeform Webhook

Typeform webhooks allow you to send real-time data to a specified URL whenever a form is submitted.

  1. Access Form Webhook Settings:
  • Log in to your Typeform account.
  • Select the form you want to integrate.
  • Go to the “Connect” panel from the top navigation.
  • Find and select “Webhooks” from the left-hand menu.

    1. Add a New Webhook:
  • Click “Add a webhook”.

  • For the “URL” field, you’ll temporarily use your ngrok public URL (e.g., https://<random_string>.ngrok-free.app/webhook). If you have a deployed server, use its public URL.

  • Optionally, you can set a “Secret” for verifying webhook requests. This adds an extra layer of security. If you set one, store it as TYPEFORM_WEBHOOK_SECRET.

  • Leave “Legacy API version” unchecked.

  • Click “Save Webhook”.

Typeform will immediately send a test request to your URL. Your local Flask server (which we’ll set up next) needs to be running to receive this test.

3. Develop the Python Webhook Listener (Flask)

This Flask application will listen for Typeform webhooks, parse the incoming data, and send a formatted message to your Discord channel.

First, create a file named config.env in your project directory to store your sensitive credentials:

DISCORD_BOT_TOKEN=<YOUR_DISCORD_BOT_TOKEN>
DISCORD_CHANNEL_ID=<YOUR_DISCORD_CHANNEL_ID>
TYPEFORM_WEBHOOK_SECRET=<YOUR_TYPEFORM_WEBHOOK_SECRET> # Optional, remove line if not used
Enter fullscreen mode Exit fullscreen mode

Next, create your Python script, for example, app.py:

import os
import requests
import json
from flask import Flask, request, abort
from dotenv import load_dotenv

# Load environment variables from config.env
load_dotenv('config.env')

app = Flask(__name__)

# --- Configuration ---
DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN')
DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID')
TYPEFORM_WEBHOOK_SECRET = os.getenv('TYPEFORM_WEBHOOK_SECRET') # Optional

DISCORD_API_URL = f"https://discord.com/api/v10/channels/{DISCORD_CHANNEL_ID}/messages"

if not DISCORD_BOT_TOKEN or not DISCORD_CHANNEL_ID:
    print("Error: DISCORD_BOT_TOKEN or DISCORD_CHANNEL_ID not set in config.env")
    exit(1)

# --- Helper Functions ---

def send_discord_message(content, embed_data=None):
    headers = {
        "Authorization": f"Bot {DISCORD_BOT_TOKEN}",
        "Content-Type": "application/json"
    }
    payload = {"content": content}
    if embed_data:
        payload["embeds"] = [embed_data]

    try:
        response = requests.post(DISCORD_API_URL, headers=headers, json=payload)
        response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)
        print(f"Discord message sent successfully: {response.status_code}")
        return True
    except requests.exceptions.HTTPError as err:
        print(f"HTTP error sending Discord message: {err}")
        print(f"Response content: {response.text}")
    except requests.exceptions.RequestException as err:
        print(f"Error sending Discord message: {err}")
    return False

def format_typeform_response_for_discord(form_response):
    form_title = form_response['definition']['title']
    submission_time = form_response['submitted_at']
    response_token = form_response['token']

    # Create a dictionary for easier lookup of field titles
    field_definitions = {field['id']: field for field in form_response['definition']['fields']}

    description_parts = []
    for answer in form_response['answers']:
        field_id = answer['field']['id']
        field_title = field_definitions.get(field_id, {}).get('title', f"Unknown Field ({field_id})")

        answer_value = "N/A"
        # Extract the correct answer value based on its type
        if 'text' in answer:
            answer_value = answer['text']
        elif 'email' in answer:
            answer_value = answer['email']
        elif 'url' in answer:
            answer_value = answer['url']
        elif 'choice' in answer:
            answer_value = answer['choice']['label'] if 'label' in answer['choice'] else answer['choice']['id']
        elif 'choices' in answer: # For multiple choice
            answer_value = ", ".join([c['label'] for c in answer['choices']])
        elif 'number' in answer:
            answer_value = str(answer['number'])
        elif 'date' in answer:
            answer_value = answer['date']
        elif 'boolean' in answer:
            answer_value = "Yes" if answer['boolean'] else "No"
        # Add more types as needed

        description_parts.append(f"**{field_title}**: {answer_value}")

    embed = {
        "title": f"New Submission: {form_title}",
        "description": "\n".join(description_parts),
        "color": 3447003, # A nice blue color for Discord embeds
        "fields": [
            {
                "name": "Submitted At",
                "value": submission_time,
                "inline": True
            },
            {
                "name": "Response Token",
                "value": response_token,
                "inline": True
            }
        ],
        "footer": {
            "text": "Via TechResolve Typeform Sync"
        }
    }
    return embed

# --- Flask Routes ---

@app.route('/webhook', methods=['POST'])
def typeform_webhook():
    if not request.is_json:
        app.logger.warning("Webhook received non-JSON content.")
        abort(400, description="Content-Type must be application/json")

    # Optional: Verify webhook signature for security
    # if TYPEFORM_WEBHOOK_SECRET:
    #     signature = request.headers.get('Typeform-Signature')
    #     if not signature:
    #         app.logger.warning("Missing Typeform-Signature header.")
    #         abort(401, description="Missing signature")
    #     # Implement actual signature verification logic here
    #     # This usually involves hashing the raw request body with the secret
    #     # and comparing it to the signature header.
    #     # For this basic tutorial, we'll omit the complex verification
    #     # but it's highly recommended for production environments.
    #     pass

    try:
        payload = request.get_json()
        app.logger.info(f"Received Typeform webhook payload: {json.dumps(payload, indent=2)}")

        if 'event_type' in payload and payload['event_type'] == 'form_response':
            form_response = payload['form_response']

            discord_embed = format_typeform_response_for_discord(form_response)
            send_discord_message(content="A new Typeform response has been received!", embed_data=discord_embed)

            return {"status": "success", "message": "Webhook processed and Discord message sent."}, 200
        else:
            app.logger.warning(f"Received unexpected event type: {payload.get('event_type')}")
            return {"status": "ignored", "message": "Not a form_response event."}, 200

    except Exception as e:
        app.logger.error(f"Error processing webhook: {e}", exc_info=True)
        abort(500, description="Internal server error during webhook processing.")

if __name__ == '__main__':
    # For local development, run with debug=True
    # For production, use a WSGI server like Gunicorn or uWSGI
    app.run(debug=True, port=5000)
Enter fullscreen mode Exit fullscreen mode

Explanation of the Code Logic:

  • config.env: Securely loads your Discord bot token, channel ID, and Typeform secret without hardcoding them into your script.
  • Flask Application (app.py):
    • Initializes a Flask app.
    • Defines DISCORD_BOT_TOKEN, DISCORD_CHANNEL_ID, and TYPEFORM_WEBHOOK_SECRET from the environment variables.
    • The /webhook endpoint listens for POST requests. Typeform sends its data as a POST request to this URL.
    • It first checks if the request content type is JSON.
    • It then parses the JSON payload received from Typeform.
    • It specifically looks for event_type == 'form_response' to ensure it’s processing a new submission.
    • format_typeform_response_for_discord: This crucial function takes the raw Typeform form_response data. It iterates through the answers, matching them with their respective question titles from the definition section of the payload. This ensures your Discord message is human-readable, showing “Question: Answer” pairs rather than just raw field IDs. It constructs a rich Discord Embed for better presentation.
    • send_discord_message: This function uses the requests library to make a POST request to the Discord API. It includes your bot token in the Authorization header and sends the formatted message (or embed) as JSON. It also includes basic error handling for API calls.
    • Finally, the app.run() command starts the Flask development server on port 5000.

4. Expose Your Webhook Endpoint (ngrok) and Test

Your Flask application is running locally, but Typeform needs a publicly accessible URL to send webhooks to. This is where ngrok comes in.

  1. Start Your Flask App:
  • Open your terminal or command prompt.
  • Navigate to the directory where you saved app.py and config.env.
  • Run your Flask application:

     python app.py
    

    You should see output indicating the Flask server is running, usually on http://127.0.0.1:5000.

    1. Expose with ngrok:
  • Open a new terminal window (keep your Flask app running in the first one).

  • Run ngrok, forwarding traffic to your Flask app’s port (5000):

     ngrok http 5000
    
  • ngrok will provide a public URL (e.g., https://<random_string>.ngrok-free.app). Copy the https URL.

    1. Update Typeform Webhook URL:
  • Go back to your Typeform webhook settings (Step 2).

  • Edit your webhook and update the “URL” field with the ngrok https URL, appending /webhook to it (e.g., https://<random_string>.ngrok-free.app/webhook).

  • Save the webhook. Typeform will send a test request.

    1. Test the Integration:
  • Fill out and submit your Typeform form.

  • Check your Flask app’s terminal for logs indicating it received the webhook.

  • Check your Discord admin channel; you should see a new message from your bot containing the form submission details!

Common Pitfalls

  • Incorrect API Keys/Tokens/IDs: This is the most frequent issue. Double-check your DISCORD_BOT_TOKEN (ensure it starts with “Bot “), DISCORD_CHANNEL_ID, and any TYPEFORM_WEBHOOK_SECRET. Permissions for the Discord bot are also critical; ensure it has Send Messages.
  • Webhook URL Mismatch: Ensure the URL configured in Typeform exactly matches the public URL provided by ngrok (or your deployed server), including the /webhook path. If using ngrok, remember its URL changes every time you restart it (unless you have a paid plan).
  • Network/Firewall Issues: If deploying to a server, ensure that port 5000 (or whichever port your Flask app uses) is open to incoming traffic from the internet, and that your server can make outgoing HTTPS requests to discord.com. For local testing, ensure no local firewalls are blocking ngrok or your Flask app.
  • Typeform Webhook Signature Verification: If you enabled a webhook secret in Typeform, your Flask app must implement signature verification. The provided example omits this for brevity, but it’s a critical security measure for production environments to ensure requests are genuinely from Typeform. Without it, anyone could send POST requests to your endpoint.

Conclusion

You’ve successfully built an automated system to push Typeform responses directly into your private Discord admin channel! This integration significantly reduces manual effort, provides your team with real-time insights, and ensures critical information is immediately accessible where your team communicates. By automating this process, you free up valuable time that can be better spent on more complex DevOps tasks or feature development.

As a next step, consider deploying this Flask application to a production environment. This could be a small cloud instance, a Docker container on Kubernetes, or even transforming it into a serverless function (e.g., AWS Lambda with an API Gateway, Google Cloud Functions, or Azure Functions) to minimize operational overhead and scale cost-effectively. You might also want to enhance the Discord message formatting, add more robust error logging, or integrate with other tools in your DevOps arsenal. The possibilities for expanding this foundation are endless!


Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)