Skip to content

HTTP Webhooks ​

Receive events via HTTP POST requests to your webhook endpoint.

Overview ​

HTTP webhooks are the simplest way to integrate with Flow. The service sends events as JSON in HTTP POST requests to your endpoint.

How It Works ​

Endpoint Requirements ​

Your webhook endpoint must:

  1. Accept POST requests at a public URL
  2. Parse JSON from the request body
  3. Return JSON actions or 204 No Content
  4. Respond as quickly as possible
  5. Use HTTPS in production

Request Format ​

All requests are POST with JSON body:

http
POST /webhook HTTP/1.1
Host: your-domain.com
Content-Type: application/json
X-API-Key: your-api-key (optional)

{
  "type": "user_speak",
  "text": "Hello",
  "session": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "account_id": "account-123",
    "phone_number": "1234567890",
    "from_phone_number": "9876543210",
    "to_phone_number": "1234567890"
  }
}

Response Format ​

Return an Action ​

http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "type": "speak",
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "text": "Hello! How can I help you?"
}

No Action Needed ​

http
HTTP/1.1 204 No Content

A 200 OK with an empty body, an empty object ({}), or an empty array ([]) works just as well — all of them simply mean "do nothing for this event". This is handy with no-code tools that always return a JSON body.

Implementation Examples ​

Python (Flask) ​

python
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    event = request.json

    # Handle different event types
    if event['type'] == 'session_start':
        return jsonify({
            'type': 'speak',
            'session_id': event['session']['id'],
            'text': 'Welcome!'
        })

    elif event['type'] == 'user_speak':
        return jsonify({
            'type': 'speak',
            'session_id': event['session']['id'],
            'text': f"You said: {event['text']}"
        })

    # No response needed
    return '', 204

if __name__ == '__main__':
    app.run(port=3000)

Node.js (Express) ​

javascript
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhook', (req, res) => {
  const event = req.body;

  if (event.type === 'session_start') {
    return res.json({
      type: 'speak',
      session_id: event.session.id,
      text: 'Welcome!'
    });
  }

  if (event.type === 'user_speak') {
    return res.json({
      type: 'speak',
      session_id: event.session.id,
      text: `You said: ${event.text}`
    });
  }

  res.status(204).send();
});

app.listen(3000);

Go ​

go
package main

import (
    "encoding/json"
    "net/http"
)

type Event struct {
    Type    string  `json:"type"`
    Text    string  `json:"text,omitempty"`
    Session Session `json:"session"`
}

type Session struct {
    ID string `json:"id"`
}

func webhook(w http.ResponseWriter, r *http.Request) {
    var event Event
    json.NewDecoder(r.Body).Decode(&event)

    if event.Type == "user_speak" {
        action := map[string]interface{}{
            "type":       "speak",
            "session_id": event.Session.ID,
            "text":       "You said: " + event.Text,
        }
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(action)
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

func main() {
    http.HandleFunc("/webhook", webhook)
    http.ListenAndServe(":3000", nil)
}

Ruby (Sinatra) ​

ruby
require 'sinatra'
require 'json'

post '/webhook' do
  event = JSON.parse(request.body.read)

  if event['type'] == 'user_speak'
    return JSON.generate({
      type: 'speak',
      session_id: event['session']['id'],
      text: "You said: #{event['text']}"
    })
  end

  status 204
end

Error Handling ​

Handle errors gracefully:

python
@app.route('/webhook', methods=['POST'])
def webhook():
    try:
        event = request.json

        if not event or 'type' not in event:
            return jsonify({'error': 'Invalid event'}), 400

        # Process event
        # ...

    except Exception as e:
        print(f"Error processing webhook: {e}")
        return jsonify({'error': 'Internal server error'}), 500

Best Practices ​

  1. Idempotency - Handle duplicate events gracefully
  2. Async Processing - Process long-running tasks asynchronously
  3. Logging - Log all events for debugging
  4. Validation - Validate event structure
  5. Error Responses - Return appropriate HTTP status codes

Testing Locally ​

Use a tunneling service to expose your local server:

bash
# Using ngrok
ngrok http 3000

# Using localtunnel
npx localtunnel --port 3000

Then configure the tunnel URL in your Flow dashboard.

Production Deployment ​

Deploy to any platform that supports HTTP:

  • AWS Lambda - Serverless functions
  • Google Cloud Functions - Serverless
  • Heroku - Platform as a service
  • Railway - Modern deployment
  • Your own server - VPS, dedicated server

Next Steps ​